@trackunit/react-map 0.2.33-alpha-288640a1f26.0 → 0.2.35
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/index.cjs.js +128 -42
- package/index.esm.js +128 -42
- package/migrations/entry.js.map +1 -0
- package/package.json +10 -10
- package/src/appearance/AppearancePreviewGrid.d.ts +3 -1
- package/src/controls/collapseControls.d.ts +6 -4
- package/src/controls/types.d.ts +9 -2
- /package/{translation10.cjs.js → translation.cjs10.js} +0 -0
- /package/{translation11.cjs.js → translation.cjs11.js} +0 -0
- /package/{translation12.cjs.js → translation.cjs12.js} +0 -0
- /package/{translation13.cjs.js → translation.cjs13.js} +0 -0
- /package/{translation14.cjs.js → translation.cjs14.js} +0 -0
- /package/{translation15.cjs.js → translation.cjs15.js} +0 -0
- /package/{translation16.cjs.js → translation.cjs16.js} +0 -0
- /package/{translation17.cjs.js → translation.cjs17.js} +0 -0
- /package/{translation2.cjs.js → translation.cjs2.js} +0 -0
- /package/{translation3.cjs.js → translation.cjs3.js} +0 -0
- /package/{translation4.cjs.js → translation.cjs4.js} +0 -0
- /package/{translation5.cjs.js → translation.cjs5.js} +0 -0
- /package/{translation6.cjs.js → translation.cjs6.js} +0 -0
- /package/{translation7.cjs.js → translation.cjs7.js} +0 -0
- /package/{translation8.cjs.js → translation.cjs8.js} +0 -0
- /package/{translation9.cjs.js → translation.cjs9.js} +0 -0
- /package/{translation10.esm.js → translation.esm10.js} +0 -0
- /package/{translation11.esm.js → translation.esm11.js} +0 -0
- /package/{translation12.esm.js → translation.esm12.js} +0 -0
- /package/{translation13.esm.js → translation.esm13.js} +0 -0
- /package/{translation14.esm.js → translation.esm14.js} +0 -0
- /package/{translation15.esm.js → translation.esm15.js} +0 -0
- /package/{translation16.esm.js → translation.esm16.js} +0 -0
- /package/{translation17.esm.js → translation.esm17.js} +0 -0
- /package/{translation2.esm.js → translation.esm2.js} +0 -0
- /package/{translation3.esm.js → translation.esm3.js} +0 -0
- /package/{translation4.esm.js → translation.esm4.js} +0 -0
- /package/{translation5.esm.js → translation.esm5.js} +0 -0
- /package/{translation6.esm.js → translation.esm6.js} +0 -0
- /package/{translation7.esm.js → translation.esm7.js} +0 -0
- /package/{translation8.esm.js → translation.esm8.js} +0 -0
- /package/{translation9.esm.js → translation.esm9.js} +0 -0
package/index.cjs.js
CHANGED
|
@@ -59,22 +59,22 @@ const translations = {
|
|
|
59
59
|
default: defaultTranslations,
|
|
60
60
|
languages: {
|
|
61
61
|
de: () => Promise.resolve().then(function () { return require('./translation.cjs.js'); }),
|
|
62
|
-
da: () => Promise.resolve().then(function () { return require('./
|
|
63
|
-
cs: () => Promise.resolve().then(function () { return require('./
|
|
64
|
-
nl: () => Promise.resolve().then(function () { return require('./
|
|
65
|
-
fr: () => Promise.resolve().then(function () { return require('./
|
|
66
|
-
fi: () => Promise.resolve().then(function () { return require('./
|
|
67
|
-
hu: () => Promise.resolve().then(function () { return require('./
|
|
68
|
-
it: () => Promise.resolve().then(function () { return require('./
|
|
69
|
-
nb: () => Promise.resolve().then(function () { return require('./
|
|
70
|
-
pl: () => Promise.resolve().then(function () { return require('./
|
|
71
|
-
pt: () => Promise.resolve().then(function () { return require('./
|
|
72
|
-
ru: () => Promise.resolve().then(function () { return require('./
|
|
73
|
-
ro: () => Promise.resolve().then(function () { return require('./
|
|
74
|
-
es: () => Promise.resolve().then(function () { return require('./
|
|
75
|
-
sv: () => Promise.resolve().then(function () { return require('./
|
|
76
|
-
ja: () => Promise.resolve().then(function () { return require('./
|
|
77
|
-
th: () => Promise.resolve().then(function () { return require('./
|
|
62
|
+
da: () => Promise.resolve().then(function () { return require('./translation.cjs2.js'); }),
|
|
63
|
+
cs: () => Promise.resolve().then(function () { return require('./translation.cjs3.js'); }),
|
|
64
|
+
nl: () => Promise.resolve().then(function () { return require('./translation.cjs4.js'); }),
|
|
65
|
+
fr: () => Promise.resolve().then(function () { return require('./translation.cjs5.js'); }),
|
|
66
|
+
fi: () => Promise.resolve().then(function () { return require('./translation.cjs6.js'); }),
|
|
67
|
+
hu: () => Promise.resolve().then(function () { return require('./translation.cjs7.js'); }),
|
|
68
|
+
it: () => Promise.resolve().then(function () { return require('./translation.cjs8.js'); }),
|
|
69
|
+
nb: () => Promise.resolve().then(function () { return require('./translation.cjs9.js'); }),
|
|
70
|
+
pl: () => Promise.resolve().then(function () { return require('./translation.cjs10.js'); }),
|
|
71
|
+
pt: () => Promise.resolve().then(function () { return require('./translation.cjs11.js'); }),
|
|
72
|
+
ru: () => Promise.resolve().then(function () { return require('./translation.cjs12.js'); }),
|
|
73
|
+
ro: () => Promise.resolve().then(function () { return require('./translation.cjs13.js'); }),
|
|
74
|
+
es: () => Promise.resolve().then(function () { return require('./translation.cjs14.js'); }),
|
|
75
|
+
sv: () => Promise.resolve().then(function () { return require('./translation.cjs15.js'); }),
|
|
76
|
+
ja: () => Promise.resolve().then(function () { return require('./translation.cjs16.js'); }),
|
|
77
|
+
th: () => Promise.resolve().then(function () { return require('./translation.cjs17.js'); }),
|
|
78
78
|
},
|
|
79
79
|
};
|
|
80
80
|
/**
|
|
@@ -1579,14 +1579,43 @@ const usePreviewMap = (adapterConfig, options) => {
|
|
|
1579
1579
|
return react.useMemo(() => ({ Map, ready }), [Map, ready]);
|
|
1580
1580
|
};
|
|
1581
1581
|
|
|
1582
|
+
/** Shared separator between control/category blocks. */
|
|
1583
|
+
const SEPARATOR = { type: "separator" };
|
|
1582
1584
|
// ============================================================================
|
|
1583
1585
|
// Overflow Behavior Helper
|
|
1584
1586
|
// ============================================================================
|
|
1585
1587
|
/** Resolve a control's overflow behavior, defaulting to "menu". */
|
|
1586
1588
|
const resolveOverflow = (control) => control.overflowBehavior ?? "menu";
|
|
1587
|
-
/** Filter out controls that
|
|
1589
|
+
/** Filter out controls that opted out of menuing with `overflowBehavior: "hide"`. */
|
|
1588
1590
|
const excludeHidden = (controls) => controls.filter(c => resolveOverflow(c) !== "hide");
|
|
1589
1591
|
// ============================================================================
|
|
1592
|
+
// Section name as a Fallback Header
|
|
1593
|
+
// ============================================================================
|
|
1594
|
+
/**
|
|
1595
|
+
* Fallback `section` across the block's leading run of items that declare none of their
|
|
1596
|
+
* own -- from the first non-separator item up to the first separator or the first item
|
|
1597
|
+
* with its own `section` (ADR 0027).
|
|
1598
|
+
*
|
|
1599
|
+
* The whole run gets it so the header and the items it visually claims land in one
|
|
1600
|
+
* `groupItemsBySection` group. The run stops at a separator because a separator always
|
|
1601
|
+
* starts a new group, so carrying the section past one would repeat the header.
|
|
1602
|
+
*/
|
|
1603
|
+
const applyFallbackSectionAsHeader = (items, section) => {
|
|
1604
|
+
const runStart = items.findIndex(item => item.type !== "separator");
|
|
1605
|
+
const firstItem = items[runStart];
|
|
1606
|
+
if (firstItem === undefined || firstItem.type === "separator" || firstItem.section !== undefined)
|
|
1607
|
+
return items;
|
|
1608
|
+
const runEndOffset = items
|
|
1609
|
+
.slice(runStart + 1)
|
|
1610
|
+
.findIndex(item => item.type === "separator" || item.section !== undefined);
|
|
1611
|
+
const runEnd = runEndOffset === -1 ? items.length : runStart + 1 + runEndOffset;
|
|
1612
|
+
return items.map((item, index) => {
|
|
1613
|
+
if (index < runStart || index >= runEnd || item.type === "separator")
|
|
1614
|
+
return item;
|
|
1615
|
+
return { ...item, section };
|
|
1616
|
+
});
|
|
1617
|
+
};
|
|
1618
|
+
// ============================================================================
|
|
1590
1619
|
// Control-to-MenuItem Conversion
|
|
1591
1620
|
// ============================================================================
|
|
1592
1621
|
/**
|
|
@@ -1596,10 +1625,10 @@ const excludeHidden = (controls) => controls.filter(c => resolveOverflow(c) !==
|
|
|
1596
1625
|
* One special case:
|
|
1597
1626
|
* - **Menu** → flattens its items into the parent (menus cannot nest)
|
|
1598
1627
|
*
|
|
1599
|
-
*
|
|
1600
|
-
*
|
|
1628
|
+
* Items keep whatever `section` their author declared. The category label is
|
|
1629
|
+
* applied later, once per block, by {@link applyFallbackSectionAsHeader}.
|
|
1601
1630
|
*/
|
|
1602
|
-
const controlToMenuItems = (control
|
|
1631
|
+
const controlToMenuItems = (control) => {
|
|
1603
1632
|
switch (control.type) {
|
|
1604
1633
|
case "button":
|
|
1605
1634
|
case "toggle":
|
|
@@ -1610,14 +1639,10 @@ const controlToMenuItems = (control, section) => {
|
|
|
1610
1639
|
case "search":
|
|
1611
1640
|
case "custom":
|
|
1612
1641
|
case "stepper":
|
|
1613
|
-
return [
|
|
1642
|
+
return [control];
|
|
1614
1643
|
case "menu":
|
|
1615
|
-
//
|
|
1616
|
-
return control.items
|
|
1617
|
-
if (item.type === "separator")
|
|
1618
|
-
return item;
|
|
1619
|
-
return section !== undefined ? { ...item, section } : item;
|
|
1620
|
-
});
|
|
1644
|
+
// Menus cannot nest -- flatten the menu's items into the parent
|
|
1645
|
+
return control.items;
|
|
1621
1646
|
default: {
|
|
1622
1647
|
const exhaustiveCheck = control;
|
|
1623
1648
|
throw new Error(`Unknown control type: ${exhaustiveCheck}`);
|
|
@@ -1625,6 +1650,60 @@ const controlToMenuItems = (control, section) => {
|
|
|
1625
1650
|
}
|
|
1626
1651
|
};
|
|
1627
1652
|
// ============================================================================
|
|
1653
|
+
// Flattening a Set of Controls Into One Menu
|
|
1654
|
+
// ============================================================================
|
|
1655
|
+
/**
|
|
1656
|
+
* Trim edge separators and collapse runs (so authored separators do not double up
|
|
1657
|
+
* against delimiters from {@link flattenControlsToMenuItems}).
|
|
1658
|
+
*/
|
|
1659
|
+
const normalizeSeparators = (items) => {
|
|
1660
|
+
const normalized = items.reduce((accumulated, item) => {
|
|
1661
|
+
if (item.type !== "separator")
|
|
1662
|
+
return [...accumulated, item];
|
|
1663
|
+
if (accumulated.length === 0 || accumulated.at(-1)?.type === "separator")
|
|
1664
|
+
return accumulated;
|
|
1665
|
+
return [...accumulated, SEPARATOR];
|
|
1666
|
+
}, []);
|
|
1667
|
+
return normalized.at(-1)?.type === "separator" ? normalized.slice(0, -1) : normalized;
|
|
1668
|
+
};
|
|
1669
|
+
/**
|
|
1670
|
+
* Whether a control type renders as one row inside a menu: two adjacent single-row
|
|
1671
|
+
* controls skip the separator between them, anything else is delimited (ADR 0027).
|
|
1672
|
+
*
|
|
1673
|
+
* Total `Record` so a new `ControlConfig` type fails compile until classified.
|
|
1674
|
+
*/
|
|
1675
|
+
const RENDERS_AS_SINGLE_ROW = {
|
|
1676
|
+
button: true,
|
|
1677
|
+
checkbox: true,
|
|
1678
|
+
stepper: true,
|
|
1679
|
+
toggle: true,
|
|
1680
|
+
custom: false,
|
|
1681
|
+
menu: false,
|
|
1682
|
+
radioGroup: false,
|
|
1683
|
+
search: false,
|
|
1684
|
+
select: false,
|
|
1685
|
+
toggleGroup: false,
|
|
1686
|
+
};
|
|
1687
|
+
const isSingleRowControl = (control) => RENDERS_AS_SINGLE_ROW[control.type];
|
|
1688
|
+
/**
|
|
1689
|
+
* Flatten controls into one menu's items, preserving declared order (ADR 0022).
|
|
1690
|
+
*
|
|
1691
|
+
* Separator between adjacent controls unless both render as a single row
|
|
1692
|
+
* ({@link RENDERS_AS_SINGLE_ROW}). Empty/separator-only controls contribute no
|
|
1693
|
+
* control block (ADR 0027).
|
|
1694
|
+
*/
|
|
1695
|
+
const flattenControlsToMenuItems = (controls) => {
|
|
1696
|
+
const controlBlocks = controls
|
|
1697
|
+
.map(control => ({ isSingleRow: isSingleRowControl(control), items: controlToMenuItems(control) }))
|
|
1698
|
+
.filter(controlBlock => controlBlock.items.some(item => item.type !== "separator"));
|
|
1699
|
+
const flattened = controlBlocks.flatMap((controlBlock, index) => {
|
|
1700
|
+
const previousBlock = controlBlocks[index - 1];
|
|
1701
|
+
const needsSeparator = previousBlock !== undefined && !(previousBlock.isSingleRow && controlBlock.isSingleRow);
|
|
1702
|
+
return needsSeparator ? [SEPARATOR, ...controlBlock.items] : controlBlock.items;
|
|
1703
|
+
});
|
|
1704
|
+
return normalizeSeparators(flattened);
|
|
1705
|
+
};
|
|
1706
|
+
// ============================================================================
|
|
1628
1707
|
// Category Collapsing (categorized mode)
|
|
1629
1708
|
// ============================================================================
|
|
1630
1709
|
/**
|
|
@@ -1640,7 +1719,7 @@ const collapseCategory = (controls, categoryKey, meta) => {
|
|
|
1640
1719
|
return controls;
|
|
1641
1720
|
// Exclude controls that prefer hiding over being menued
|
|
1642
1721
|
const menuable = excludeHidden(controls);
|
|
1643
|
-
const menuItems = menuable
|
|
1722
|
+
const menuItems = flattenControlsToMenuItems(menuable);
|
|
1644
1723
|
// If nothing remains after filtering, return empty
|
|
1645
1724
|
if (menuItems.length === 0)
|
|
1646
1725
|
return [];
|
|
@@ -1657,21 +1736,17 @@ const collapseCategory = (controls, categoryKey, meta) => {
|
|
|
1657
1736
|
return [menu];
|
|
1658
1737
|
};
|
|
1659
1738
|
/**
|
|
1660
|
-
*
|
|
1661
|
-
*
|
|
1739
|
+
* Merge all categories into one menu. Non-empty category blocks are
|
|
1740
|
+
* separator-delimited; labels via {@link applyFallbackSectionAsHeader}.
|
|
1662
1741
|
*
|
|
1663
1742
|
* Controls with `overflowBehavior: "hide"` are excluded and fully hidden.
|
|
1664
1743
|
*/
|
|
1665
1744
|
const collapseAll = (categories, allControls) => {
|
|
1666
|
-
const
|
|
1667
|
-
for (const { label, controls } of categories) {
|
|
1668
|
-
if (controls.length === 0)
|
|
1669
|
-
continue;
|
|
1745
|
+
const categoryBlocks = categories
|
|
1670
1746
|
// Exclude controls that prefer hiding over being menued
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
}
|
|
1747
|
+
.map(({ label, controls }) => applyFallbackSectionAsHeader(flattenControlsToMenuItems(excludeHidden(controls)), label))
|
|
1748
|
+
.filter(items => items.length > 0);
|
|
1749
|
+
const allItems = categoryBlocks.flatMap((items, index) => (index === 0 ? items : [SEPARATOR, ...items]));
|
|
1675
1750
|
// Nothing to show
|
|
1676
1751
|
if (allItems.length === 0) {
|
|
1677
1752
|
return {
|
|
@@ -1706,7 +1781,9 @@ const collapseAll = (categories, allControls) => {
|
|
|
1706
1781
|
* - `"full"` -- pass through unchanged
|
|
1707
1782
|
* - `"categorized"` -- group controls within each category into a per-category
|
|
1708
1783
|
* menu (only when >1 control in that category)
|
|
1709
|
-
* - `"collapsed"` -- merge all controls into a single menu
|
|
1784
|
+
* - `"collapsed"` -- merge all controls into a single menu, separator-delimited per
|
|
1785
|
+
* category. Items keep their own `section`; the category name only heads a block whose
|
|
1786
|
+
* leading items declare none (ADR 0027)
|
|
1710
1787
|
*
|
|
1711
1788
|
* This operates at the config level, before controls enter the middleware
|
|
1712
1789
|
* rendering pipeline. Existing renderers handle the resulting menus with no
|
|
@@ -2291,6 +2368,9 @@ const renderMenuItem = (item, index, availableSpace, customPortalId, isFullScree
|
|
|
2291
2368
|
const groupItemsBySection = (items) => {
|
|
2292
2369
|
const groups = [];
|
|
2293
2370
|
for (const item of items) {
|
|
2371
|
+
// Separators get their own group so they delimit rather than join one: a separator declares no
|
|
2372
|
+
// `section`, so without this branch its `undefined` matches the preceding sectionless group and
|
|
2373
|
+
// `[a, separator, b]` would collapse from three groups into one holding all three.
|
|
2294
2374
|
if (item.type === "separator") {
|
|
2295
2375
|
groups.push({ section: undefined, items: [item] });
|
|
2296
2376
|
continue;
|
|
@@ -2667,7 +2747,7 @@ const AppearancePreview = ({ adapterConfig, mapType, label, selected, onSelect,
|
|
|
2667
2747
|
*
|
|
2668
2748
|
* @internal
|
|
2669
2749
|
*/
|
|
2670
|
-
const AppearancePreviewGrid = ({ adapterConfig, options, currentTheme, currentMapType, onChange, }) => {
|
|
2750
|
+
const AppearancePreviewGrid = ({ adapterConfig, ariaLabel, options, currentTheme, currentMapType, onChange, }) => {
|
|
2671
2751
|
const derivedConfigs = react.useMemo(() => {
|
|
2672
2752
|
const configs = new Map();
|
|
2673
2753
|
for (const option of options) {
|
|
@@ -2677,7 +2757,7 @@ const AppearancePreviewGrid = ({ adapterConfig, options, currentTheme, currentMa
|
|
|
2677
2757
|
}
|
|
2678
2758
|
return configs;
|
|
2679
2759
|
}, [adapterConfig, options]);
|
|
2680
|
-
return (jsxRuntime.jsx("div", { className: "flex flex-wrap items-start gap-2 px-3 py-2", role: "radiogroup", children: options.map(option => {
|
|
2760
|
+
return (jsxRuntime.jsx("div", { "aria-label": ariaLabel, className: "flex flex-wrap items-start gap-2 px-3 py-2", role: "radiogroup", children: options.map(option => {
|
|
2681
2761
|
const derivedConfig = derivedConfigs.get(option.theme);
|
|
2682
2762
|
if (derivedConfig === undefined) {
|
|
2683
2763
|
return null;
|
|
@@ -2872,7 +2952,13 @@ const useMapAppearanceControls = ({ api, options: appearanceOptions, persistence
|
|
|
2872
2952
|
type: "custom",
|
|
2873
2953
|
id: "map-style-preview",
|
|
2874
2954
|
label: t("settings.mapStyle.title"),
|
|
2875
|
-
|
|
2955
|
+
// Heads the grid in every mode. In a flattened responsive menu the trigger label is
|
|
2956
|
+
// gone, so without this the grid renders anonymously under the previous control's
|
|
2957
|
+
// items; in "full" mode it makes this menu consistent with Assets and Sites.
|
|
2958
|
+
section: t("settings.mapStyle.title"),
|
|
2959
|
+
render: () => (jsxRuntime.jsx(AppearancePreviewGrid, { adapterConfig: adapterConfig,
|
|
2960
|
+
// `SectionHeader` is role="presentation", so the header above names nothing.
|
|
2961
|
+
ariaLabel: t("settings.mapStyle.title"), currentMapType: localState.mapType, currentTheme: localState.theme, onChange: handleAppearanceChange, options: previewOptions })),
|
|
2876
2962
|
},
|
|
2877
2963
|
];
|
|
2878
2964
|
const isSatelliteOrHybrid = localState.mapType === "satellite" || localState.mapType === "hybrid";
|
package/index.esm.js
CHANGED
|
@@ -58,22 +58,22 @@ const translations = {
|
|
|
58
58
|
default: defaultTranslations,
|
|
59
59
|
languages: {
|
|
60
60
|
de: () => import('./translation.esm.js'),
|
|
61
|
-
da: () => import('./
|
|
62
|
-
cs: () => import('./
|
|
63
|
-
nl: () => import('./
|
|
64
|
-
fr: () => import('./
|
|
65
|
-
fi: () => import('./
|
|
66
|
-
hu: () => import('./
|
|
67
|
-
it: () => import('./
|
|
68
|
-
nb: () => import('./
|
|
69
|
-
pl: () => import('./
|
|
70
|
-
pt: () => import('./
|
|
71
|
-
ru: () => import('./
|
|
72
|
-
ro: () => import('./
|
|
73
|
-
es: () => import('./
|
|
74
|
-
sv: () => import('./
|
|
75
|
-
ja: () => import('./
|
|
76
|
-
th: () => import('./
|
|
61
|
+
da: () => import('./translation.esm2.js'),
|
|
62
|
+
cs: () => import('./translation.esm3.js'),
|
|
63
|
+
nl: () => import('./translation.esm4.js'),
|
|
64
|
+
fr: () => import('./translation.esm5.js'),
|
|
65
|
+
fi: () => import('./translation.esm6.js'),
|
|
66
|
+
hu: () => import('./translation.esm7.js'),
|
|
67
|
+
it: () => import('./translation.esm8.js'),
|
|
68
|
+
nb: () => import('./translation.esm9.js'),
|
|
69
|
+
pl: () => import('./translation.esm10.js'),
|
|
70
|
+
pt: () => import('./translation.esm11.js'),
|
|
71
|
+
ru: () => import('./translation.esm12.js'),
|
|
72
|
+
ro: () => import('./translation.esm13.js'),
|
|
73
|
+
es: () => import('./translation.esm14.js'),
|
|
74
|
+
sv: () => import('./translation.esm15.js'),
|
|
75
|
+
ja: () => import('./translation.esm16.js'),
|
|
76
|
+
th: () => import('./translation.esm17.js'),
|
|
77
77
|
},
|
|
78
78
|
};
|
|
79
79
|
/**
|
|
@@ -1578,14 +1578,43 @@ const usePreviewMap = (adapterConfig, options) => {
|
|
|
1578
1578
|
return useMemo(() => ({ Map, ready }), [Map, ready]);
|
|
1579
1579
|
};
|
|
1580
1580
|
|
|
1581
|
+
/** Shared separator between control/category blocks. */
|
|
1582
|
+
const SEPARATOR = { type: "separator" };
|
|
1581
1583
|
// ============================================================================
|
|
1582
1584
|
// Overflow Behavior Helper
|
|
1583
1585
|
// ============================================================================
|
|
1584
1586
|
/** Resolve a control's overflow behavior, defaulting to "menu". */
|
|
1585
1587
|
const resolveOverflow = (control) => control.overflowBehavior ?? "menu";
|
|
1586
|
-
/** Filter out controls that
|
|
1588
|
+
/** Filter out controls that opted out of menuing with `overflowBehavior: "hide"`. */
|
|
1587
1589
|
const excludeHidden = (controls) => controls.filter(c => resolveOverflow(c) !== "hide");
|
|
1588
1590
|
// ============================================================================
|
|
1591
|
+
// Section name as a Fallback Header
|
|
1592
|
+
// ============================================================================
|
|
1593
|
+
/**
|
|
1594
|
+
* Fallback `section` across the block's leading run of items that declare none of their
|
|
1595
|
+
* own -- from the first non-separator item up to the first separator or the first item
|
|
1596
|
+
* with its own `section` (ADR 0027).
|
|
1597
|
+
*
|
|
1598
|
+
* The whole run gets it so the header and the items it visually claims land in one
|
|
1599
|
+
* `groupItemsBySection` group. The run stops at a separator because a separator always
|
|
1600
|
+
* starts a new group, so carrying the section past one would repeat the header.
|
|
1601
|
+
*/
|
|
1602
|
+
const applyFallbackSectionAsHeader = (items, section) => {
|
|
1603
|
+
const runStart = items.findIndex(item => item.type !== "separator");
|
|
1604
|
+
const firstItem = items[runStart];
|
|
1605
|
+
if (firstItem === undefined || firstItem.type === "separator" || firstItem.section !== undefined)
|
|
1606
|
+
return items;
|
|
1607
|
+
const runEndOffset = items
|
|
1608
|
+
.slice(runStart + 1)
|
|
1609
|
+
.findIndex(item => item.type === "separator" || item.section !== undefined);
|
|
1610
|
+
const runEnd = runEndOffset === -1 ? items.length : runStart + 1 + runEndOffset;
|
|
1611
|
+
return items.map((item, index) => {
|
|
1612
|
+
if (index < runStart || index >= runEnd || item.type === "separator")
|
|
1613
|
+
return item;
|
|
1614
|
+
return { ...item, section };
|
|
1615
|
+
});
|
|
1616
|
+
};
|
|
1617
|
+
// ============================================================================
|
|
1589
1618
|
// Control-to-MenuItem Conversion
|
|
1590
1619
|
// ============================================================================
|
|
1591
1620
|
/**
|
|
@@ -1595,10 +1624,10 @@ const excludeHidden = (controls) => controls.filter(c => resolveOverflow(c) !==
|
|
|
1595
1624
|
* One special case:
|
|
1596
1625
|
* - **Menu** → flattens its items into the parent (menus cannot nest)
|
|
1597
1626
|
*
|
|
1598
|
-
*
|
|
1599
|
-
*
|
|
1627
|
+
* Items keep whatever `section` their author declared. The category label is
|
|
1628
|
+
* applied later, once per block, by {@link applyFallbackSectionAsHeader}.
|
|
1600
1629
|
*/
|
|
1601
|
-
const controlToMenuItems = (control
|
|
1630
|
+
const controlToMenuItems = (control) => {
|
|
1602
1631
|
switch (control.type) {
|
|
1603
1632
|
case "button":
|
|
1604
1633
|
case "toggle":
|
|
@@ -1609,14 +1638,10 @@ const controlToMenuItems = (control, section) => {
|
|
|
1609
1638
|
case "search":
|
|
1610
1639
|
case "custom":
|
|
1611
1640
|
case "stepper":
|
|
1612
|
-
return [
|
|
1641
|
+
return [control];
|
|
1613
1642
|
case "menu":
|
|
1614
|
-
//
|
|
1615
|
-
return control.items
|
|
1616
|
-
if (item.type === "separator")
|
|
1617
|
-
return item;
|
|
1618
|
-
return section !== undefined ? { ...item, section } : item;
|
|
1619
|
-
});
|
|
1643
|
+
// Menus cannot nest -- flatten the menu's items into the parent
|
|
1644
|
+
return control.items;
|
|
1620
1645
|
default: {
|
|
1621
1646
|
const exhaustiveCheck = control;
|
|
1622
1647
|
throw new Error(`Unknown control type: ${exhaustiveCheck}`);
|
|
@@ -1624,6 +1649,60 @@ const controlToMenuItems = (control, section) => {
|
|
|
1624
1649
|
}
|
|
1625
1650
|
};
|
|
1626
1651
|
// ============================================================================
|
|
1652
|
+
// Flattening a Set of Controls Into One Menu
|
|
1653
|
+
// ============================================================================
|
|
1654
|
+
/**
|
|
1655
|
+
* Trim edge separators and collapse runs (so authored separators do not double up
|
|
1656
|
+
* against delimiters from {@link flattenControlsToMenuItems}).
|
|
1657
|
+
*/
|
|
1658
|
+
const normalizeSeparators = (items) => {
|
|
1659
|
+
const normalized = items.reduce((accumulated, item) => {
|
|
1660
|
+
if (item.type !== "separator")
|
|
1661
|
+
return [...accumulated, item];
|
|
1662
|
+
if (accumulated.length === 0 || accumulated.at(-1)?.type === "separator")
|
|
1663
|
+
return accumulated;
|
|
1664
|
+
return [...accumulated, SEPARATOR];
|
|
1665
|
+
}, []);
|
|
1666
|
+
return normalized.at(-1)?.type === "separator" ? normalized.slice(0, -1) : normalized;
|
|
1667
|
+
};
|
|
1668
|
+
/**
|
|
1669
|
+
* Whether a control type renders as one row inside a menu: two adjacent single-row
|
|
1670
|
+
* controls skip the separator between them, anything else is delimited (ADR 0027).
|
|
1671
|
+
*
|
|
1672
|
+
* Total `Record` so a new `ControlConfig` type fails compile until classified.
|
|
1673
|
+
*/
|
|
1674
|
+
const RENDERS_AS_SINGLE_ROW = {
|
|
1675
|
+
button: true,
|
|
1676
|
+
checkbox: true,
|
|
1677
|
+
stepper: true,
|
|
1678
|
+
toggle: true,
|
|
1679
|
+
custom: false,
|
|
1680
|
+
menu: false,
|
|
1681
|
+
radioGroup: false,
|
|
1682
|
+
search: false,
|
|
1683
|
+
select: false,
|
|
1684
|
+
toggleGroup: false,
|
|
1685
|
+
};
|
|
1686
|
+
const isSingleRowControl = (control) => RENDERS_AS_SINGLE_ROW[control.type];
|
|
1687
|
+
/**
|
|
1688
|
+
* Flatten controls into one menu's items, preserving declared order (ADR 0022).
|
|
1689
|
+
*
|
|
1690
|
+
* Separator between adjacent controls unless both render as a single row
|
|
1691
|
+
* ({@link RENDERS_AS_SINGLE_ROW}). Empty/separator-only controls contribute no
|
|
1692
|
+
* control block (ADR 0027).
|
|
1693
|
+
*/
|
|
1694
|
+
const flattenControlsToMenuItems = (controls) => {
|
|
1695
|
+
const controlBlocks = controls
|
|
1696
|
+
.map(control => ({ isSingleRow: isSingleRowControl(control), items: controlToMenuItems(control) }))
|
|
1697
|
+
.filter(controlBlock => controlBlock.items.some(item => item.type !== "separator"));
|
|
1698
|
+
const flattened = controlBlocks.flatMap((controlBlock, index) => {
|
|
1699
|
+
const previousBlock = controlBlocks[index - 1];
|
|
1700
|
+
const needsSeparator = previousBlock !== undefined && !(previousBlock.isSingleRow && controlBlock.isSingleRow);
|
|
1701
|
+
return needsSeparator ? [SEPARATOR, ...controlBlock.items] : controlBlock.items;
|
|
1702
|
+
});
|
|
1703
|
+
return normalizeSeparators(flattened);
|
|
1704
|
+
};
|
|
1705
|
+
// ============================================================================
|
|
1627
1706
|
// Category Collapsing (categorized mode)
|
|
1628
1707
|
// ============================================================================
|
|
1629
1708
|
/**
|
|
@@ -1639,7 +1718,7 @@ const collapseCategory = (controls, categoryKey, meta) => {
|
|
|
1639
1718
|
return controls;
|
|
1640
1719
|
// Exclude controls that prefer hiding over being menued
|
|
1641
1720
|
const menuable = excludeHidden(controls);
|
|
1642
|
-
const menuItems = menuable
|
|
1721
|
+
const menuItems = flattenControlsToMenuItems(menuable);
|
|
1643
1722
|
// If nothing remains after filtering, return empty
|
|
1644
1723
|
if (menuItems.length === 0)
|
|
1645
1724
|
return [];
|
|
@@ -1656,21 +1735,17 @@ const collapseCategory = (controls, categoryKey, meta) => {
|
|
|
1656
1735
|
return [menu];
|
|
1657
1736
|
};
|
|
1658
1737
|
/**
|
|
1659
|
-
*
|
|
1660
|
-
*
|
|
1738
|
+
* Merge all categories into one menu. Non-empty category blocks are
|
|
1739
|
+
* separator-delimited; labels via {@link applyFallbackSectionAsHeader}.
|
|
1661
1740
|
*
|
|
1662
1741
|
* Controls with `overflowBehavior: "hide"` are excluded and fully hidden.
|
|
1663
1742
|
*/
|
|
1664
1743
|
const collapseAll = (categories, allControls) => {
|
|
1665
|
-
const
|
|
1666
|
-
for (const { label, controls } of categories) {
|
|
1667
|
-
if (controls.length === 0)
|
|
1668
|
-
continue;
|
|
1744
|
+
const categoryBlocks = categories
|
|
1669
1745
|
// Exclude controls that prefer hiding over being menued
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
}
|
|
1746
|
+
.map(({ label, controls }) => applyFallbackSectionAsHeader(flattenControlsToMenuItems(excludeHidden(controls)), label))
|
|
1747
|
+
.filter(items => items.length > 0);
|
|
1748
|
+
const allItems = categoryBlocks.flatMap((items, index) => (index === 0 ? items : [SEPARATOR, ...items]));
|
|
1674
1749
|
// Nothing to show
|
|
1675
1750
|
if (allItems.length === 0) {
|
|
1676
1751
|
return {
|
|
@@ -1705,7 +1780,9 @@ const collapseAll = (categories, allControls) => {
|
|
|
1705
1780
|
* - `"full"` -- pass through unchanged
|
|
1706
1781
|
* - `"categorized"` -- group controls within each category into a per-category
|
|
1707
1782
|
* menu (only when >1 control in that category)
|
|
1708
|
-
* - `"collapsed"` -- merge all controls into a single menu
|
|
1783
|
+
* - `"collapsed"` -- merge all controls into a single menu, separator-delimited per
|
|
1784
|
+
* category. Items keep their own `section`; the category name only heads a block whose
|
|
1785
|
+
* leading items declare none (ADR 0027)
|
|
1709
1786
|
*
|
|
1710
1787
|
* This operates at the config level, before controls enter the middleware
|
|
1711
1788
|
* rendering pipeline. Existing renderers handle the resulting menus with no
|
|
@@ -2290,6 +2367,9 @@ const renderMenuItem = (item, index, availableSpace, customPortalId, isFullScree
|
|
|
2290
2367
|
const groupItemsBySection = (items) => {
|
|
2291
2368
|
const groups = [];
|
|
2292
2369
|
for (const item of items) {
|
|
2370
|
+
// Separators get their own group so they delimit rather than join one: a separator declares no
|
|
2371
|
+
// `section`, so without this branch its `undefined` matches the preceding sectionless group and
|
|
2372
|
+
// `[a, separator, b]` would collapse from three groups into one holding all three.
|
|
2293
2373
|
if (item.type === "separator") {
|
|
2294
2374
|
groups.push({ section: undefined, items: [item] });
|
|
2295
2375
|
continue;
|
|
@@ -2666,7 +2746,7 @@ const AppearancePreview = ({ adapterConfig, mapType, label, selected, onSelect,
|
|
|
2666
2746
|
*
|
|
2667
2747
|
* @internal
|
|
2668
2748
|
*/
|
|
2669
|
-
const AppearancePreviewGrid = ({ adapterConfig, options, currentTheme, currentMapType, onChange, }) => {
|
|
2749
|
+
const AppearancePreviewGrid = ({ adapterConfig, ariaLabel, options, currentTheme, currentMapType, onChange, }) => {
|
|
2670
2750
|
const derivedConfigs = useMemo(() => {
|
|
2671
2751
|
const configs = new Map();
|
|
2672
2752
|
for (const option of options) {
|
|
@@ -2676,7 +2756,7 @@ const AppearancePreviewGrid = ({ adapterConfig, options, currentTheme, currentMa
|
|
|
2676
2756
|
}
|
|
2677
2757
|
return configs;
|
|
2678
2758
|
}, [adapterConfig, options]);
|
|
2679
|
-
return (jsx("div", { className: "flex flex-wrap items-start gap-2 px-3 py-2", role: "radiogroup", children: options.map(option => {
|
|
2759
|
+
return (jsx("div", { "aria-label": ariaLabel, className: "flex flex-wrap items-start gap-2 px-3 py-2", role: "radiogroup", children: options.map(option => {
|
|
2680
2760
|
const derivedConfig = derivedConfigs.get(option.theme);
|
|
2681
2761
|
if (derivedConfig === undefined) {
|
|
2682
2762
|
return null;
|
|
@@ -2871,7 +2951,13 @@ const useMapAppearanceControls = ({ api, options: appearanceOptions, persistence
|
|
|
2871
2951
|
type: "custom",
|
|
2872
2952
|
id: "map-style-preview",
|
|
2873
2953
|
label: t("settings.mapStyle.title"),
|
|
2874
|
-
|
|
2954
|
+
// Heads the grid in every mode. In a flattened responsive menu the trigger label is
|
|
2955
|
+
// gone, so without this the grid renders anonymously under the previous control's
|
|
2956
|
+
// items; in "full" mode it makes this menu consistent with Assets and Sites.
|
|
2957
|
+
section: t("settings.mapStyle.title"),
|
|
2958
|
+
render: () => (jsx(AppearancePreviewGrid, { adapterConfig: adapterConfig,
|
|
2959
|
+
// `SectionHeader` is role="presentation", so the header above names nothing.
|
|
2960
|
+
ariaLabel: t("settings.mapStyle.title"), currentMapType: localState.mapType, currentTheme: localState.theme, onChange: handleAppearanceChange, options: previewOptions })),
|
|
2875
2961
|
},
|
|
2876
2962
|
];
|
|
2877
2963
|
const isSatelliteOrHybrid = localState.mapType === "satellite" || localState.mapType === "hybrid";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entry.js","sourceRoot":"","sources":["../../../../../libs/react/map/migrations/entry.ts"],"names":[],"mappings":"","sourcesContent":["export {};\n"]}
|
package/package.json
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-map",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.35",
|
|
4
4
|
"repository": "https://github.com/Trackunit/manager",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=24.x"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@trackunit/react-components": "2.2.26
|
|
11
|
-
"@trackunit/css-class-variance-utilities": "1.13.
|
|
12
|
-
"@trackunit/react-form-components": "2.2.
|
|
13
|
-
"@trackunit/react-core-hooks": "1.18.
|
|
14
|
-
"@trackunit/geo-json-utils": "1.14.
|
|
15
|
-
"@trackunit/i18n-library-translation": "2.1.
|
|
10
|
+
"@trackunit/react-components": "2.2.26",
|
|
11
|
+
"@trackunit/css-class-variance-utilities": "1.13.82",
|
|
12
|
+
"@trackunit/react-form-components": "2.2.28",
|
|
13
|
+
"@trackunit/react-core-hooks": "1.18.23",
|
|
14
|
+
"@trackunit/geo-json-utils": "1.14.85",
|
|
15
|
+
"@trackunit/i18n-library-translation": "2.1.23",
|
|
16
16
|
"react-minimal-pie-chart": "^8.4.0",
|
|
17
|
-
"@trackunit/react-map-adapter-shared": "0.0.
|
|
18
|
-
"@trackunit/react-map-color-utils": "0.0.
|
|
19
|
-
"@trackunit/ui-design-tokens": "1.13.
|
|
17
|
+
"@trackunit/react-map-adapter-shared": "0.0.66",
|
|
18
|
+
"@trackunit/react-map-color-utils": "0.0.51",
|
|
19
|
+
"@trackunit/ui-design-tokens": "1.13.81",
|
|
20
20
|
"@floating-ui/react": "^0.26.25",
|
|
21
21
|
"es-toolkit": "^1.39.10",
|
|
22
22
|
"tailwind-merge": "^2.0.0",
|
|
@@ -2,6 +2,8 @@ import type { AdapterConfig } from "@trackunit/react-map-adapter-shared";
|
|
|
2
2
|
import type { MapTheme, MapType } from "../core/types";
|
|
3
3
|
import type { AppearanceOption } from "./appearanceTypes";
|
|
4
4
|
type AppearancePreviewGridProps = Readonly<{
|
|
5
|
+
/** Accessible name for the radiogroup (the visual `SectionHeader` is `role="presentation"`) */
|
|
6
|
+
ariaLabel: string;
|
|
5
7
|
/** The adapter config from the main map (used to derive preview configs) */
|
|
6
8
|
adapterConfig: AdapterConfig;
|
|
7
9
|
/** Appearance options to display */
|
|
@@ -25,5 +27,5 @@ type AppearancePreviewGridProps = Readonly<{
|
|
|
25
27
|
*
|
|
26
28
|
* @internal
|
|
27
29
|
*/
|
|
28
|
-
export declare const AppearancePreviewGrid: ({ adapterConfig, options, currentTheme, currentMapType, onChange, }: AppearancePreviewGridProps) => import("react/jsx-runtime").JSX.Element;
|
|
30
|
+
export declare const AppearancePreviewGrid: ({ adapterConfig, ariaLabel, options, currentTheme, currentMapType, onChange, }: AppearancePreviewGridProps) => import("react/jsx-runtime").JSX.Element;
|
|
29
31
|
export {};
|
|
@@ -16,10 +16,10 @@ export type CategoryMetadata = Readonly<Record<CategoryKey, CategoryMeta> & {
|
|
|
16
16
|
* One special case:
|
|
17
17
|
* - **Menu** → flattens its items into the parent (menus cannot nest)
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* Items keep whatever `section` their author declared. The category label is
|
|
20
|
+
* applied later, once per block, by {@link applyFallbackSectionAsHeader}.
|
|
21
21
|
*/
|
|
22
|
-
export declare const controlToMenuItems: (control: ControlConfig
|
|
22
|
+
export declare const controlToMenuItems: (control: ControlConfig) => ReadonlyArray<MenuItemConfig>;
|
|
23
23
|
/**
|
|
24
24
|
* Pure config transform that restructures `ControlsConfig` based on how much
|
|
25
25
|
* space is available.
|
|
@@ -27,7 +27,9 @@ export declare const controlToMenuItems: (control: ControlConfig, section?: stri
|
|
|
27
27
|
* - `"full"` -- pass through unchanged
|
|
28
28
|
* - `"categorized"` -- group controls within each category into a per-category
|
|
29
29
|
* menu (only when >1 control in that category)
|
|
30
|
-
* - `"collapsed"` -- merge all controls into a single menu
|
|
30
|
+
* - `"collapsed"` -- merge all controls into a single menu, separator-delimited per
|
|
31
|
+
* category. Items keep their own `section`; the category name only heads a block whose
|
|
32
|
+
* leading items declare none (ADR 0027)
|
|
31
33
|
*
|
|
32
34
|
* This operates at the config level, before controls enter the middleware
|
|
33
35
|
* rendering pipeline. Existing renderers handle the resulting menus with no
|
package/src/controls/types.d.ts
CHANGED
|
@@ -284,6 +284,11 @@ export type CustomControlConfig = BaseControlConfig<"custom"> & Readonly<{
|
|
|
284
284
|
* Use this to transform or extend the resolved props based on the environment.
|
|
285
285
|
* For example, override size decisions or add custom visibility logic.
|
|
286
286
|
*
|
|
287
|
+
* **Caveat —** returning `visible: false` hides the control at render time only.
|
|
288
|
+
* Responsive flattening, its separators and its section headers are computed
|
|
289
|
+
* beforehand and do not account for it, so a control hidden this way inside an
|
|
290
|
+
* overflow menu can leave an orphaned divider or section header behind.
|
|
291
|
+
*
|
|
287
292
|
* @example
|
|
288
293
|
* ```ts
|
|
289
294
|
* resolve: (resolved, renderContext) => ({
|
|
@@ -356,8 +361,10 @@ export interface BuiltInControlsConfig extends ControlsConfig {
|
|
|
356
361
|
* - `"full"` -- all standalone controls shown as-is (wide containers)
|
|
357
362
|
* - `"categorized"` -- controls within each category grouped into a per-category
|
|
358
363
|
* menu when >1 control exists in that category (medium containers)
|
|
359
|
-
* - `"collapsed"` -- all controls collapsed into a single menu button with
|
|
360
|
-
*
|
|
364
|
+
* - `"collapsed"` -- all controls collapsed into a single menu button, with each
|
|
365
|
+
* category's block separated from the next (narrow containers). Items keep whatever
|
|
366
|
+
* `section` they declare; the category name is only used as a fallback header for a
|
|
367
|
+
* block whose leading items declare none.
|
|
361
368
|
*/
|
|
362
369
|
export type ResponsiveMode = "full" | "categorized" | "collapsed";
|
|
363
370
|
/** The four semantic control areas used for categorization and section headers */
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|