layers-select-control 0.3.0 → 0.4.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/README.md CHANGED
@@ -5,11 +5,13 @@ A high-performance React component for selecting layers, inspired by the **Googl
5
5
  ## Features
6
6
 
7
7
  * **Google Maps UI:** Seamlessly transitions between a collapsed thumbnail and an expanded list.
8
- * **Intelligent Image Loading:** The `ManagedImage` system handles HD transitions and fallbacks without UI flickering.
9
- * **Interactive Scrolling:** Automatically provides navigation arrows for horizontal scrolling when items exceed `maxVisible`.
10
- * **Theming & Sizing:** Built-in `dark` and `light` themes with three size presets (`small`, `medium`, `large`).
11
- * **Adaptive Positioning:** Easily anchor the control to any corner of its parent container.
12
- * **Mobile Optimized:** Includes touch-start detection and pointer-event handling for mobile maps.
8
+ * **Intelligent Image Loading:** The `ManagedImage` system handles HD transitions and fallbacks (`thumbnailHd` -> `thumbnail` -> `noImageThumb`) without UI flickering.
9
+ * **Ref/Imperative API:** Access internal state and control selection programmatically using `useImperativeHandle`.
10
+ * **Adaptive Positioning:** Anchor the control to any corner using `xRel` and `yRel`.
11
+ * **Dynamic Sizing:** Built-in `small`, `medium`, and `large` presets that scale all dimensions via CSS variables.
12
+ * **Interactive Scrolling:** Automatically detects overflow and provides navigation arrows for horizontal scrolling.
13
+ * **Lifecycle Hooks:** `onExpand` and `onCollapse` callbacks for integration with other UI elements.
14
+ * **Touch Optimized:** Includes pointer-event handling and touch-start detection for mobile map environments.
13
15
 
14
16
  ## Installation
15
17
 
@@ -28,22 +30,25 @@ The component is highly configurable to fit different map layouts and branding r
28
30
 
29
31
  | Prop | Type | Default | Description |
30
32
  | --- | --- | --- | --- |
31
- | `items` | `LayerItem[]` | **Required** | Array of items to display in the panel. |
33
+ | `items` | `LayerItem[]` | **Required** | Array of items to display in the main scrollable area. |
32
34
  | `onSelect` | `(item: LayerItem) => void` | **Required** | Callback triggered when a layer is selected. |
33
35
  | `onDefault` | `(item?: LayerItem) => void` | **Required** | Callback for the "Default" tile click. |
34
36
  | `value` | `string` | - | **Controlled mode**: The ID of the currently selected item. |
35
37
  | `defaultValue` | `string` | - | **Uncontrolled mode**: The initial ID to be selected. |
36
- | `defaultItem` | `LayerItem` | - | Optional item that appears as the "Default" tile. |
37
- | `size` | `"small" | "medium" | "large"` | `"small"` | Adjusts dimensions of the control and tiles. |
38
- | `theme` | `"dark" | "light"` | `"dark"` | Built-in color scheme. |
39
- | `maxVisible` | `number` | `4` | Number of items visible before enabling horizontal scroll. |
38
+ | `defaultItem` | `LayerItem` | - | A special tile that appears fixed at the start of the list. |
39
+ | `moreItem` | `LayerItem` | - | Custom title/description for the "More" tile. |
40
+ | `size` | `"small" | "medium" | "large"` |
41
+ | `theme` | `"dark" | "light"` | `"dark"` |
42
+ | `maxVisible` | `number` | `5` | Number of items visible before enabling horizontal scroll. |
40
43
  | `x`, `y` | `number` | `8`, `32` | Numerical offset from the anchor point. |
41
- | `xRel` | `"left" | "right"` | `"left"` | Horizontal anchor (relative to parent). |
42
- | `yRel` | `"top" | "bottom"` | `"bottom"` | Vertical anchor (relative to parent). |
44
+ | `xRel` | `"left" | "right"` | `"left"` |
45
+ | `yRel` | `"top" | "bottom"` | `"bottom"` |
43
46
  | `onMore` | `() => void` | - | If provided, a "More..." tile is appended to the list. |
44
- | `hoverCloseDelayMs` | `number` | `160` | Delay before closing the panel on `mouseleave`. |
45
- | `panelGap` | `number` | `8` | Space between the collapsed thumb and the panel. |
46
- | `parentGap` | `number` | `8` | Margin maintained between the panel and the parent edge. |
47
+ | `onExpand` | `() => void` | - | Fired when the panel opens. |
48
+ | `onCollapse` | `() => void` | - | Fired when the panel closes. |
49
+ | `defaultThumb` | `string` | - | Override the default "reset" icon URL. |
50
+ | `noImageThumb` | `string` | - | Fallback image for items without thumbnails. |
51
+ | `moreThumb` | `string` | - | Icon for the "More" tile. |
47
52
 
48
53
  ## Data Interfaces
49
54
 
@@ -55,7 +60,19 @@ export interface LayerItem {
55
60
  title: string;
56
61
  description?: string;
57
62
  thumbnail?: string; // Standard thumbnail
58
- thumbnailHd?: string; // High-quality version loaded when expanded
63
+ thumbnailHd?: string; // HD version loaded only when panel expands
64
+ }
65
+
66
+ ```
67
+
68
+ ### Imperative Handle (Ref)
69
+
70
+ You can control the component externally by passing a `ref`:
71
+
72
+ ```ts
73
+ export interface LayersSelectControlRef {
74
+ setSelectedItem: (id: string) => void;
75
+ getSelectedItem: () => LayerItem | undefined;
59
76
  }
60
77
 
61
78
  ```
@@ -63,8 +80,8 @@ export interface LayerItem {
63
80
  ## Usage Example
64
81
 
65
82
  ```tsx
66
- import React from "react";
67
- import { LayersSelectControl, LayerItem } from "layers-select-control";
83
+ import React, { useRef } from "react";
84
+ import { LayersSelectControl, LayerItem, LayersSelectControlRef } from "layers-select-control";
68
85
 
69
86
  const LAYERS: LayerItem[] = [
70
87
  {
@@ -75,17 +92,20 @@ const LAYERS: LayerItem[] = [
75
92
  thumbnailHd: "/thumbs/sat-hd.jpg"
76
93
  },
77
94
  { id: "terrain", title: "Terrain", description: "Topographic maps", thumbnail: "/thumbs/terrain.jpg" },
78
- { id: "streets", title: "Streets", description: "Urban navigation", thumbnail: "/thumbs/streets.jpg" },
79
95
  ];
80
96
 
81
97
  export const App = () => {
98
+ const controlRef = useRef<LayersSelectControlRef>(null);
99
+
82
100
  return (
83
- <div style={{ position: "relative", width: "100%", height: "500px", background: "#eee" }}>
101
+ <div style={{ position: "relative", width: "100%", height: "600px" }}>
84
102
  <LayersSelectControl
103
+ ref={controlRef}
85
104
  items={LAYERS}
86
105
  size="medium"
87
106
  theme="light"
88
- onSelect={(item) => console.log("Selected:", item.title)}
107
+ defaultItem={{ id: "base", title: "Standard", description: "Default Map" }}
108
+ onSelect={(item) => console.log("Active Layer:", item.id)}
89
109
  onDefault={() => console.log("Reset to default")}
90
110
  onMore={() => alert("Open full catalog")}
91
111
  />
@@ -97,10 +117,11 @@ export const App = () => {
97
117
 
98
118
  ## Styling & Theme
99
119
 
100
- The component uses scoped CSS injected via a `<style>` tag, ensuring no styles leak out to the rest of your application. It supports:
120
+ The component uses scoped CSS logic to manage layouts dynamically:
101
121
 
102
- * **Automatic Overflow:** Handles parent container width constraints using `ResizeObserver`.
103
- * **Responsive Scaling:** Sizes automatically adjust based on the `size` prop.
122
+ * **Responsive Scaling:** Uses internal configurations to calculate exact pixel dimensions for thumbnails and tiles.
123
+ * **Auto-Width:** Utilizes `ResizeObserver` on the parent container to ensure the expanded panel never overflows the screen boundaries.
124
+ * **Pointer Awareness:** Automatically switches between `hover` and `touch` events depending on the user's device.
104
125
 
105
126
  ## License
106
127
 
@@ -7,9 +7,11 @@ export interface LayerItem {
7
7
  thumbnailHd?: string;
8
8
  }
9
9
  interface Props {
10
+ title?: string;
10
11
  value?: string;
11
12
  defaultValue?: string;
12
13
  defaultItem?: LayerItem;
14
+ externalItem?: LayerItem;
13
15
  moreItem?: LayerItem;
14
16
  items: LayerItem[];
15
17
  x?: number;
@@ -31,6 +33,10 @@ interface Props {
31
33
  onExpand?: () => void;
32
34
  onCollapse?: () => void;
33
35
  }
34
- export declare const LayersSelectControl: React.FC<Props>;
36
+ export interface LayersSelectControlRef {
37
+ setSelectedItem: (id: string) => void;
38
+ getSelectedItem: () => LayerItem | undefined;
39
+ }
40
+ export declare const LayersSelectControl: React.ForwardRefExoticComponent<Props & React.RefAttributes<LayersSelectControlRef>>;
35
41
  export {};
36
42
  //# sourceMappingURL=LayersSelectControl.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"LayersSelectControl.d.ts","sourceRoot":"","sources":["../src/LayersSelectControl.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAEjF,MAAM,WAAW,SAAS;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,UAAU,KAAK;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,EAAE,SAAS,EAAE,CAAC;IAEnB,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACxB,IAAI,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IAExB,SAAS,EAAE,CAAC,IAAI,CAAC,EAAE,SAAS,KAAK,IAAI,CAAC;IACtC,QAAQ,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IAEpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAEzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,IAAI,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B;AAkED,eAAO,MAAM,mBAAmB,EAAE,KAAK,CAAC,EAAE,CAAC,KAAK,CA0Q/C,CAAC"}
1
+ {"version":3,"file":"LayersSelectControl.d.ts","sourceRoot":"","sources":["../src/LayersSelectControl.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAEjF,MAAM,WAAW,SAAS;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,UAAU,KAAK;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,EAAE,SAAS,EAAE,CAAC;IAEnB,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACxB,IAAI,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IAExB,SAAS,EAAE,CAAC,IAAI,CAAC,EAAE,SAAS,KAAK,IAAI,CAAC;IACtC,QAAQ,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IAEpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAEzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,IAAI,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B;AA+DD,MAAM,WAAW,sBAAsB;IACnC,eAAe,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,eAAe,EAAE,MAAM,SAAS,GAAG,SAAS,CAAC;CAChD;AAKD,eAAO,MAAM,mBAAmB,sFAoR9B,CAAC"}
@@ -1,8 +1,41 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.LayersSelectControl = void 0;
4
37
  const jsx_runtime_1 = require("react/jsx-runtime");
5
- const react_1 = require("react");
38
+ const react_1 = __importStar(require("react"));
6
39
  const DEFAULT_RESET_IMAGE_URL = "./firstplace.svg";
7
40
  const DEFAULT_NO_IMAGE_URL = "./noimage.png";
8
41
  const DEFAULT_IMAGE_MORE = "./more-images.png";
@@ -47,7 +80,7 @@ function ManagedImage({ candidates, initial, alt, className, expanded, noImageTh
47
80
  // ------------------------------------------------------
48
81
  // COMPONENT (LOGIC PRESERVED)
49
82
  // ------------------------------------------------------
50
- const LayersSelectControl = ({ items, value, defaultValue, x = 8, y = 32, xRel = "left", yRel = "bottom", defaultItem, moreItem, onSelect, onMore, onDefault, maxVisible = DEFAULT_MAX_VISIBLE, hoverCloseDelayMs = 160, theme = "dark", defaultThumb = DEFAULT_RESET_IMAGE_URL, noImageThumb = DEFAULT_NO_IMAGE_URL, moreThumb = DEFAULT_IMAGE_MORE, size = "small", panelGap = 8, parentGap = 8, onCollapse, onExpand }) => {
83
+ exports.LayersSelectControl = react_1.default.forwardRef(({ title, items, value, defaultValue, x = 8, y = 32, xRel = "left", yRel = "bottom", defaultItem, externalItem, moreItem, onSelect, onMore, onDefault, maxVisible = DEFAULT_MAX_VISIBLE, hoverCloseDelayMs = 160, theme = "dark", defaultThumb = DEFAULT_RESET_IMAGE_URL, noImageThumb = DEFAULT_NO_IMAGE_URL, moreThumb = DEFAULT_IMAGE_MORE, size = "small", panelGap = 8, parentGap = 8, onCollapse, onExpand }, ref) => {
51
84
  const isControlled = value !== undefined;
52
85
  const [internalValue, setInternalValue] = (0, react_1.useState)(defaultValue);
53
86
  const selectedId = isControlled ? value : internalValue;
@@ -108,6 +141,8 @@ const LayersSelectControl = ({ items, value, defaultValue, x = 8, y = 32, xRel =
108
141
  const selectedItem = (0, react_1.useMemo)(() => {
109
142
  if (defaultItem && selectedId === defaultItem.id)
110
143
  return defaultItem;
144
+ if (externalItem && selectedId === externalItem.id)
145
+ return externalItem;
111
146
  return items.find((x) => x.id === selectedId);
112
147
  }, [items, selectedId, defaultItem]);
113
148
  const collapsedCandidates = (0, react_1.useMemo)(() => [collapsedThumb(selectedItem)], [collapsedThumb, selectedItem]);
@@ -147,7 +182,12 @@ const LayersSelectControl = ({ items, value, defaultValue, x = 8, y = 32, xRel =
147
182
  const toggleExpanded = () => {
148
183
  setExpanded((s) => (s ? false : hasContent()));
149
184
  };
150
- return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: `lsc-root ${theme === "light" ? "lsc-light" : ""}`, style: {
185
+ // ----- Expose methods to parent -----
186
+ react_1.default.useImperativeHandle(ref, () => ({
187
+ setSelectedItem: (id) => setSelectedId(id),
188
+ getSelectedItem: () => selectedItem,
189
+ }), [selectedItem, setSelectedId]);
190
+ return ((0, jsx_runtime_1.jsxs)("div", { title: title, ref: rootRef, className: `lsc-root ${theme === "light" ? "lsc-light" : ""}`, style: {
151
191
  [xRel]: `${x}px`,
152
192
  [yRel]: `${y}px`,
153
193
  "--lsc-collapsed": `${cfg.collapsed}px`,
@@ -186,5 +226,4 @@ const LayersSelectControl = ({ items, value, defaultValue, x = 8, y = 32, xRel =
186
226
  onMore === null || onMore === void 0 ? void 0 : onMore();
187
227
  setExpanded(false);
188
228
  }, children: [(0, jsx_runtime_1.jsx)(ManagedImage, { candidates: [moreThumb], initial: moreThumb, expanded: expanded, className: "lsc-t-thumb", noImageThumb: noImageThumb }), (0, jsx_runtime_1.jsx)("div", { className: "lsc-title", children: (moreItem === null || moreItem === void 0 ? void 0 : moreItem.title) ? moreItem === null || moreItem === void 0 ? void 0 : moreItem.title : "More…" }), (0, jsx_runtime_1.jsx)("div", { className: "lsc-desc", children: (moreItem === null || moreItem === void 0 ? void 0 : moreItem.description) ? moreItem === null || moreItem === void 0 ? void 0 : moreItem.description : "Show all" })] }))] }) })] }));
189
- };
190
- exports.LayersSelectControl = LayersSelectControl;
229
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "layers-select-control",
3
3
  "description": "Mimics Google layer select layers.",
4
- "version": "0.3.0",
4
+ "version": "0.4.0",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {