layers-select-control 0.1.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 +107 -0
- package/dist/LayersSelectControl.d.ts +33 -0
- package/dist/LayersSelectControl.d.ts.map +1 -0
- package/dist/LayersSelectControl.js +177 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/styles.css +167 -0
- package/dist/styles.scss +196 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# LayersSelectControl
|
|
2
|
+
|
|
3
|
+
A high-performance React component for selecting layers, inspired by the **Google Maps** layer switcher. It provides a sleek, collapsed preview that expands into a scrollable panel with high-definition thumbnails and descriptions.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
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.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Using npm
|
|
18
|
+
npm install layers-select-control
|
|
19
|
+
|
|
20
|
+
# Using yarn
|
|
21
|
+
yarn add layers-select-control
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Props
|
|
26
|
+
|
|
27
|
+
The component is highly configurable to fit different map layouts and branding requirements.
|
|
28
|
+
|
|
29
|
+
| Prop | Type | Default | Description |
|
|
30
|
+
| --- | --- | --- | --- |
|
|
31
|
+
| `items` | `LayerItem[]` | **Required** | Array of items to display in the panel. |
|
|
32
|
+
| `onSelect` | `(item: LayerItem) => void` | **Required** | Callback triggered when a layer is selected. |
|
|
33
|
+
| `onDefault` | `(item?: LayerItem) => void` | **Required** | Callback for the "Default" tile click. |
|
|
34
|
+
| `value` | `string` | - | **Controlled mode**: The ID of the currently selected item. |
|
|
35
|
+
| `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. |
|
|
40
|
+
| `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). |
|
|
43
|
+
| `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
|
+
|
|
48
|
+
## Data Interfaces
|
|
49
|
+
|
|
50
|
+
### LayerItem
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
export interface LayerItem {
|
|
54
|
+
id: string;
|
|
55
|
+
title: string;
|
|
56
|
+
description?: string;
|
|
57
|
+
thumbnail?: string; // Standard thumbnail
|
|
58
|
+
thumbnailHd?: string; // High-quality version loaded when expanded
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Usage Example
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
import React from "react";
|
|
67
|
+
import { LayersSelectControl, LayerItem } from "layers-select-control";
|
|
68
|
+
|
|
69
|
+
const LAYERS: LayerItem[] = [
|
|
70
|
+
{
|
|
71
|
+
id: "satellite",
|
|
72
|
+
title: "Satellite",
|
|
73
|
+
description: "Global imagery",
|
|
74
|
+
thumbnail: "/thumbs/sat.jpg",
|
|
75
|
+
thumbnailHd: "/thumbs/sat-hd.jpg"
|
|
76
|
+
},
|
|
77
|
+
{ id: "terrain", title: "Terrain", description: "Topographic maps", thumbnail: "/thumbs/terrain.jpg" },
|
|
78
|
+
{ id: "streets", title: "Streets", description: "Urban navigation", thumbnail: "/thumbs/streets.jpg" },
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
export const App = () => {
|
|
82
|
+
return (
|
|
83
|
+
<div style={{ position: "relative", width: "100%", height: "500px", background: "#eee" }}>
|
|
84
|
+
<LayersSelectControl
|
|
85
|
+
items={LAYERS}
|
|
86
|
+
size="medium"
|
|
87
|
+
theme="light"
|
|
88
|
+
onSelect={(item) => console.log("Selected:", item.title)}
|
|
89
|
+
onDefault={() => console.log("Reset to default")}
|
|
90
|
+
onMore={() => alert("Open full catalog")}
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Styling & Theme
|
|
99
|
+
|
|
100
|
+
The component uses scoped CSS injected via a `<style>` tag, ensuring no styles leak out to the rest of your application. It supports:
|
|
101
|
+
|
|
102
|
+
* **Automatic Overflow:** Handles parent container width constraints using `ResizeObserver`.
|
|
103
|
+
* **Responsive Scaling:** Sizes automatically adjust based on the `size` prop.
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT © [Felipe Carrillo](https://github.com/felipecarrillo100)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface LayerItem {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
thumbnail?: string;
|
|
7
|
+
thumbnailHd?: string;
|
|
8
|
+
}
|
|
9
|
+
interface Props {
|
|
10
|
+
value?: string;
|
|
11
|
+
defaultValue?: string;
|
|
12
|
+
defaultItem?: LayerItem;
|
|
13
|
+
items: LayerItem[];
|
|
14
|
+
x?: number;
|
|
15
|
+
y?: number;
|
|
16
|
+
xRel?: "left" | "right";
|
|
17
|
+
yRel?: "top" | "bottom";
|
|
18
|
+
onDefault: (item?: LayerItem) => void;
|
|
19
|
+
onSelect: (item: LayerItem) => void;
|
|
20
|
+
onMore?: () => void;
|
|
21
|
+
maxVisible?: number;
|
|
22
|
+
hoverCloseDelayMs?: number;
|
|
23
|
+
theme?: "dark" | "light";
|
|
24
|
+
defaultThumb?: string;
|
|
25
|
+
noImageThumb?: string;
|
|
26
|
+
moreThumb?: string;
|
|
27
|
+
size?: "small" | "medium" | "large";
|
|
28
|
+
panelGap?: number;
|
|
29
|
+
parentGap?: number;
|
|
30
|
+
}
|
|
31
|
+
export declare const LayersSelectControl: React.FC<Props>;
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=LayersSelectControl.d.ts.map
|
|
@@ -0,0 +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,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;CACtB;AAiED,eAAO,MAAM,mBAAmB,EAAE,KAAK,CAAC,EAAE,CAAC,KAAK,CAuP/C,CAAC"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LayersSelectControl = void 0;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const DEFAULT_RESET_IMAGE_URL = "./firstplace.svg";
|
|
7
|
+
const DEFAULT_NO_IMAGE_URL = "./noimage.png";
|
|
8
|
+
const DEFAULT_IMAGE_MORE = "./more-images.png";
|
|
9
|
+
const DEFAULT_MAX_VISIBLE = 4;
|
|
10
|
+
const SIZE_CONFIG = {
|
|
11
|
+
small: { collapsed: 64, thumb: 56, tileWidth: 84, tileThumb: 72 },
|
|
12
|
+
medium: { collapsed: 88, thumb: 76, tileWidth: 108, tileThumb: 96 },
|
|
13
|
+
large: { collapsed: 112, thumb: 96, tileWidth: 140, tileThumb: 120 },
|
|
14
|
+
};
|
|
15
|
+
// ------------------------------------------------------
|
|
16
|
+
// ManagedImage (UNCHANGED)
|
|
17
|
+
// ------------------------------------------------------
|
|
18
|
+
function ManagedImage({ candidates, initial, alt, className, expanded, noImageThumb, }) {
|
|
19
|
+
const [finalSrc, setFinalSrc] = (0, react_1.useState)(null);
|
|
20
|
+
(0, react_1.useEffect)(() => {
|
|
21
|
+
var _a;
|
|
22
|
+
setFinalSrc(null);
|
|
23
|
+
const urls = expanded ? candidates : [(_a = initial !== null && initial !== void 0 ? initial : candidates[0]) !== null && _a !== void 0 ? _a : noImageThumb];
|
|
24
|
+
let cancelled = false;
|
|
25
|
+
const loadNext = (idx) => {
|
|
26
|
+
if (idx >= urls.length) {
|
|
27
|
+
if (!cancelled)
|
|
28
|
+
setFinalSrc(noImageThumb);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const img = new Image();
|
|
32
|
+
img.onload = () => !cancelled && setFinalSrc(urls[idx]);
|
|
33
|
+
img.onerror = () => !cancelled && loadNext(idx + 1);
|
|
34
|
+
img.src = urls[idx];
|
|
35
|
+
};
|
|
36
|
+
loadNext(0);
|
|
37
|
+
return () => {
|
|
38
|
+
cancelled = true;
|
|
39
|
+
};
|
|
40
|
+
}, [expanded, initial, candidates, noImageThumb]);
|
|
41
|
+
if (!finalSrc) {
|
|
42
|
+
return (0, jsx_runtime_1.jsx)("div", { className: className });
|
|
43
|
+
}
|
|
44
|
+
return (0, jsx_runtime_1.jsx)("img", { src: finalSrc, alt: alt, className: className, draggable: false });
|
|
45
|
+
}
|
|
46
|
+
// ------------------------------------------------------
|
|
47
|
+
// COMPONENT (LOGIC 100% PRESERVED)
|
|
48
|
+
// ------------------------------------------------------
|
|
49
|
+
const LayersSelectControl = ({ items, value, defaultValue, x = 8, y = 32, xRel = "left", yRel = "bottom", defaultItem, 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, }) => {
|
|
50
|
+
const isControlled = value !== undefined;
|
|
51
|
+
const [internalValue, setInternalValue] = (0, react_1.useState)(defaultValue);
|
|
52
|
+
const selectedId = isControlled ? value : internalValue;
|
|
53
|
+
const setSelectedId = (id) => {
|
|
54
|
+
if (!isControlled)
|
|
55
|
+
setInternalValue(id);
|
|
56
|
+
};
|
|
57
|
+
const [expanded, setExpanded] = (0, react_1.useState)(false);
|
|
58
|
+
const [isTouch, setIsTouch] = (0, react_1.useState)(false);
|
|
59
|
+
const [parentWidth, setParentWidth] = (0, react_1.useState)(null);
|
|
60
|
+
const [canScrollLeft, setCanScrollLeft] = (0, react_1.useState)(false);
|
|
61
|
+
const [canScrollRight, setCanScrollRight] = (0, react_1.useState)(false);
|
|
62
|
+
const rootRef = (0, react_1.useRef)(null);
|
|
63
|
+
const scrollRef = (0, react_1.useRef)(null);
|
|
64
|
+
const hoverTimer = (0, react_1.useRef)(null);
|
|
65
|
+
const cfg = SIZE_CONFIG[size];
|
|
66
|
+
(0, react_1.useEffect)(() => {
|
|
67
|
+
setIsTouch("ontouchstart" in window || navigator.maxTouchPoints > 0);
|
|
68
|
+
}, []);
|
|
69
|
+
(0, react_1.useEffect)(() => {
|
|
70
|
+
if (!expanded)
|
|
71
|
+
return;
|
|
72
|
+
const onPointer = (ev) => {
|
|
73
|
+
if (rootRef.current && ev.target instanceof Node && !rootRef.current.contains(ev.target)) {
|
|
74
|
+
setExpanded(false);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
document.addEventListener("pointerdown", onPointer, { capture: true });
|
|
78
|
+
return () => document.removeEventListener("pointerdown", onPointer, { capture: true });
|
|
79
|
+
}, [expanded]);
|
|
80
|
+
(0, react_1.useEffect)(() => {
|
|
81
|
+
if (!rootRef.current)
|
|
82
|
+
return;
|
|
83
|
+
const parent = rootRef.current.parentElement;
|
|
84
|
+
if (!parent)
|
|
85
|
+
return;
|
|
86
|
+
const updateWidth = () => setParentWidth(parent.clientWidth);
|
|
87
|
+
updateWidth();
|
|
88
|
+
const ro = new ResizeObserver(updateWidth);
|
|
89
|
+
ro.observe(parent);
|
|
90
|
+
return () => ro.disconnect();
|
|
91
|
+
}, []);
|
|
92
|
+
const expandToRight = xRel === "left";
|
|
93
|
+
const collapsedThumb = (0, react_1.useCallback)((it) => { var _a; return (_a = it === null || it === void 0 ? void 0 : it.thumbnail) !== null && _a !== void 0 ? _a : noImageThumb; }, [noImageThumb]);
|
|
94
|
+
const expandedCandidates = (0, react_1.useCallback)((it) => [
|
|
95
|
+
...((it === null || it === void 0 ? void 0 : it.thumbnailHd) ? [it.thumbnailHd] : []),
|
|
96
|
+
...((it === null || it === void 0 ? void 0 : it.thumbnail) ? [it.thumbnail] : []),
|
|
97
|
+
noImageThumb,
|
|
98
|
+
], [noImageThumb]);
|
|
99
|
+
const selectedItem = (0, react_1.useMemo)(() => {
|
|
100
|
+
if (defaultItem && selectedId === defaultItem.id)
|
|
101
|
+
return defaultItem;
|
|
102
|
+
return items.find((x) => x.id === selectedId);
|
|
103
|
+
}, [items, selectedId, defaultItem]);
|
|
104
|
+
const updateArrows = (0, react_1.useCallback)(() => {
|
|
105
|
+
if (!scrollRef.current)
|
|
106
|
+
return;
|
|
107
|
+
const s = scrollRef.current;
|
|
108
|
+
setCanScrollLeft(s.scrollLeft > 0);
|
|
109
|
+
setCanScrollRight(s.scrollLeft + s.clientWidth < s.scrollWidth - 1);
|
|
110
|
+
}, []);
|
|
111
|
+
const scrollBy = (dir) => {
|
|
112
|
+
var _a;
|
|
113
|
+
(_a = scrollRef.current) === null || _a === void 0 ? void 0 : _a.scrollBy({ left: dir * 120, behavior: "smooth" });
|
|
114
|
+
};
|
|
115
|
+
(0, react_1.useEffect)(() => {
|
|
116
|
+
const el = scrollRef.current;
|
|
117
|
+
if (!el)
|
|
118
|
+
return;
|
|
119
|
+
updateArrows();
|
|
120
|
+
el.addEventListener("scroll", updateArrows);
|
|
121
|
+
const ro = new ResizeObserver(updateArrows);
|
|
122
|
+
ro.observe(el);
|
|
123
|
+
window.addEventListener("resize", updateArrows);
|
|
124
|
+
return () => {
|
|
125
|
+
el.removeEventListener("scroll", updateArrows);
|
|
126
|
+
ro.disconnect();
|
|
127
|
+
window.removeEventListener("resize", updateArrows);
|
|
128
|
+
};
|
|
129
|
+
}, [updateArrows]);
|
|
130
|
+
const visibleItems = (0, react_1.useMemo)(() => {
|
|
131
|
+
if (!maxVisible || items.length <= maxVisible)
|
|
132
|
+
return items;
|
|
133
|
+
return items.slice(0, maxVisible);
|
|
134
|
+
}, [items, maxVisible]);
|
|
135
|
+
const showMore = Boolean(onMore && items.length > (maxVisible || 0));
|
|
136
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: `lsc-root ${theme === "light" ? "lsc-light" : ""}`, style: {
|
|
137
|
+
position: "absolute",
|
|
138
|
+
[xRel]: `${x}px`,
|
|
139
|
+
[yRel]: `${y}px`,
|
|
140
|
+
"--lsc-collapsed": `${cfg.collapsed}px`,
|
|
141
|
+
"--lsc-thumb": `${cfg.thumb}px`,
|
|
142
|
+
"--lsc-tile-width": `${cfg.tileWidth}px`,
|
|
143
|
+
"--lsc-tile-thumb": `${cfg.tileThumb}px`,
|
|
144
|
+
"--lsc-panel-gap": `${panelGap}px`,
|
|
145
|
+
"--lsc-parent-gap": `${parentGap}px`,
|
|
146
|
+
}, onMouseEnter: () => {
|
|
147
|
+
if (isTouch)
|
|
148
|
+
return;
|
|
149
|
+
if (hoverTimer.current)
|
|
150
|
+
clearTimeout(hoverTimer.current);
|
|
151
|
+
setExpanded(true);
|
|
152
|
+
}, onMouseLeave: () => {
|
|
153
|
+
if (isTouch)
|
|
154
|
+
return;
|
|
155
|
+
if (hoverTimer.current)
|
|
156
|
+
clearTimeout(hoverTimer.current);
|
|
157
|
+
hoverTimer.current = window.setTimeout(() => setExpanded(false), hoverCloseDelayMs);
|
|
158
|
+
}, children: [(0, jsx_runtime_1.jsx)("div", { className: "lsc-collapsed", onClick: () => setExpanded((s) => !s), children: (0, jsx_runtime_1.jsx)(ManagedImage, { candidates: [collapsedThumb(selectedItem)], initial: collapsedThumb(selectedItem), expanded: false, className: "lsc-thumb", noImageThumb: noImageThumb }) }), (0, jsx_runtime_1.jsx)("div", { className: "lsc-panel-wrap", style: {
|
|
159
|
+
left: expandToRight ? `calc(var(--lsc-collapsed) + var(--lsc-panel-gap))` : undefined,
|
|
160
|
+
right: expandToRight ? undefined : `calc(var(--lsc-collapsed) + var(--lsc-panel-gap))`,
|
|
161
|
+
maxWidth: parentWidth
|
|
162
|
+
? `${parentWidth - (x + cfg.collapsed) - panelGap - parentGap}px`
|
|
163
|
+
: undefined,
|
|
164
|
+
}, children: (0, jsx_runtime_1.jsxs)("div", { className: `lsc-panel ${expanded ? "is-open" : ""}`, children: [defaultItem && ((0, jsx_runtime_1.jsxs)("div", { className: "lsc-tile", onClick: () => {
|
|
165
|
+
setSelectedId(defaultItem.id);
|
|
166
|
+
onDefault(defaultItem);
|
|
167
|
+
setExpanded(false);
|
|
168
|
+
}, children: [(0, jsx_runtime_1.jsx)(ManagedImage, { candidates: [defaultThumb], initial: defaultThumb, expanded: expanded, className: "lsc-t-thumb", noImageThumb: noImageThumb }), (0, jsx_runtime_1.jsx)("div", { className: "lsc-title", children: "Default" }), (0, jsx_runtime_1.jsx)("div", { className: "lsc-desc", children: "Initial Content" })] })), (0, jsx_runtime_1.jsxs)("div", { className: "lsc-scroll-wrap", children: [canScrollLeft && ((0, jsx_runtime_1.jsx)("div", { className: "lsc-arrow lsc-arrow-left", onClick: () => scrollBy(-1), children: "\u276E" })), (0, jsx_runtime_1.jsx)("div", { className: "lsc-tiles-wrap", ref: scrollRef, children: (0, jsx_runtime_1.jsx)("div", { className: "lsc-tiles", children: visibleItems.map((item) => ((0, jsx_runtime_1.jsxs)("div", { className: "lsc-tile", onClick: () => {
|
|
169
|
+
setSelectedId(item.id);
|
|
170
|
+
onSelect(item);
|
|
171
|
+
setTimeout(() => setExpanded(false), 10);
|
|
172
|
+
}, children: [(0, jsx_runtime_1.jsx)(ManagedImage, { candidates: expandedCandidates(item), initial: collapsedThumb(item), expanded: expanded, className: "lsc-t-thumb", noImageThumb: noImageThumb }), (0, jsx_runtime_1.jsx)("div", { className: "lsc-title", children: item.title }), (0, jsx_runtime_1.jsx)("div", { className: "lsc-desc", children: item.description })] }, item.id))) }) }), canScrollRight && ((0, jsx_runtime_1.jsx)("div", { className: "lsc-arrow lsc-arrow-right", onClick: () => scrollBy(1), children: "\u276F" }))] }), showMore && ((0, jsx_runtime_1.jsxs)("div", { className: "lsc-tile", onClick: () => {
|
|
173
|
+
onMore === null || onMore === void 0 ? void 0 : onMore();
|
|
174
|
+
setExpanded(false);
|
|
175
|
+
}, 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: "More\u2026" }), (0, jsx_runtime_1.jsx)("div", { className: "lsc-desc", children: "Show all" })] }))] }) })] }));
|
|
176
|
+
};
|
|
177
|
+
exports.LayersSelectControl = LayersSelectControl;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LayersSelectControl = void 0;
|
|
4
|
+
// React Component
|
|
5
|
+
var LayersSelectControl_1 = require("./LayersSelectControl");
|
|
6
|
+
Object.defineProperty(exports, "LayersSelectControl", { enumerable: true, get: function () { return LayersSelectControl_1.LayersSelectControl; } });
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
@charset "UTF-8";
|
|
2
|
+
.lsc-root {
|
|
3
|
+
font-family: Inter, system-ui;
|
|
4
|
+
font-size: 13px;
|
|
5
|
+
z-index: 99999;
|
|
6
|
+
--bg: linear-gradient(180deg, #1b1f23, #111418);
|
|
7
|
+
--panel-bg: linear-gradient(180deg, #1e242a, #161b20);
|
|
8
|
+
--text: #9ca3af;
|
|
9
|
+
--subtext: #9ca3af;
|
|
10
|
+
--border: rgba(255, 255, 255, 0.08);
|
|
11
|
+
--hover: rgba(255, 255, 255, 0.08);
|
|
12
|
+
}
|
|
13
|
+
.lsc-root.lsc-light {
|
|
14
|
+
--bg: linear-gradient(180deg, #ffffff, #f7f9fb);
|
|
15
|
+
--panel-bg: linear-gradient(180deg, #ffffff, #fbfdff);
|
|
16
|
+
--text: #0f1720;
|
|
17
|
+
--subtext: #556070;
|
|
18
|
+
--border: rgba(9, 30, 66, 0.06);
|
|
19
|
+
--hover: rgba(9, 30, 66, 0.06);
|
|
20
|
+
}
|
|
21
|
+
.lsc-root .lsc-collapsed {
|
|
22
|
+
width: var(--lsc-collapsed);
|
|
23
|
+
height: var(--lsc-collapsed);
|
|
24
|
+
border-radius: 12px;
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
justify-content: center;
|
|
28
|
+
background: var(--bg);
|
|
29
|
+
border: 1px solid var(--border);
|
|
30
|
+
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.4);
|
|
31
|
+
cursor: pointer;
|
|
32
|
+
overflow: hidden;
|
|
33
|
+
}
|
|
34
|
+
.lsc-root .lsc-thumb {
|
|
35
|
+
width: var(--lsc-thumb);
|
|
36
|
+
height: calc(var(--lsc-thumb) * 0.714);
|
|
37
|
+
border-radius: 8px;
|
|
38
|
+
object-fit: cover;
|
|
39
|
+
background: rgba(0, 0, 0, 0.3333333333);
|
|
40
|
+
}
|
|
41
|
+
.lsc-root .lsc-panel-wrap {
|
|
42
|
+
position: absolute;
|
|
43
|
+
top: 50%;
|
|
44
|
+
transform: translateY(-50%);
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
max-width: 100%;
|
|
48
|
+
}
|
|
49
|
+
.lsc-root .lsc-panel {
|
|
50
|
+
align-items: center;
|
|
51
|
+
padding: 8px;
|
|
52
|
+
gap: 8px;
|
|
53
|
+
border-radius: 12px;
|
|
54
|
+
background: var(--panel-bg);
|
|
55
|
+
border: 1px solid var(--border);
|
|
56
|
+
box-shadow: 0 12px 34px rgba(0, 0, 0, 0.4);
|
|
57
|
+
display: none;
|
|
58
|
+
transform: scale(0.97);
|
|
59
|
+
pointer-events: none;
|
|
60
|
+
transition: opacity 150ms ease, transform 200ms ease;
|
|
61
|
+
overflow: hidden;
|
|
62
|
+
}
|
|
63
|
+
.lsc-root .lsc-panel.is-open {
|
|
64
|
+
display: flex;
|
|
65
|
+
transform: scale(1);
|
|
66
|
+
pointer-events: auto;
|
|
67
|
+
}
|
|
68
|
+
.lsc-root .lsc-scroll-wrap {
|
|
69
|
+
position: relative;
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
gap: 4px;
|
|
73
|
+
overflow: hidden;
|
|
74
|
+
flex: 1;
|
|
75
|
+
}
|
|
76
|
+
.lsc-root .lsc-tiles-wrap {
|
|
77
|
+
overflow-x: auto;
|
|
78
|
+
overflow-y: hidden;
|
|
79
|
+
scrollbar-width: none;
|
|
80
|
+
-ms-overflow-style: none;
|
|
81
|
+
flex: 1;
|
|
82
|
+
}
|
|
83
|
+
.lsc-root .lsc-tiles-wrap::-webkit-scrollbar {
|
|
84
|
+
display: none;
|
|
85
|
+
}
|
|
86
|
+
.lsc-root .lsc-tiles {
|
|
87
|
+
display: flex;
|
|
88
|
+
gap: 8px;
|
|
89
|
+
flex-wrap: nowrap;
|
|
90
|
+
}
|
|
91
|
+
.lsc-root .lsc-tile {
|
|
92
|
+
display: flex;
|
|
93
|
+
flex-direction: column;
|
|
94
|
+
gap: 6px;
|
|
95
|
+
cursor: pointer;
|
|
96
|
+
width: var(--lsc-tile-width);
|
|
97
|
+
padding: 4px;
|
|
98
|
+
border-radius: 8px;
|
|
99
|
+
flex-shrink: 0;
|
|
100
|
+
}
|
|
101
|
+
.lsc-root .lsc-tile:hover {
|
|
102
|
+
background: var(--hover);
|
|
103
|
+
}
|
|
104
|
+
.lsc-root .lsc-t-thumb {
|
|
105
|
+
width: var(--lsc-tile-thumb);
|
|
106
|
+
height: calc(var(--lsc-tile-thumb) * 0.667);
|
|
107
|
+
border-radius: 8px;
|
|
108
|
+
object-fit: cover;
|
|
109
|
+
background: rgba(0, 0, 0, 0.3333333333);
|
|
110
|
+
}
|
|
111
|
+
.lsc-root .lsc-title {
|
|
112
|
+
font-weight: 600;
|
|
113
|
+
color: var(--text);
|
|
114
|
+
max-width: var(--lsc-tile-thumb);
|
|
115
|
+
white-space: nowrap;
|
|
116
|
+
overflow: hidden;
|
|
117
|
+
}
|
|
118
|
+
.lsc-root .lsc-desc {
|
|
119
|
+
font-size: 11px;
|
|
120
|
+
color: var(--subtext);
|
|
121
|
+
max-width: var(--lsc-tile-thumb);
|
|
122
|
+
white-space: nowrap;
|
|
123
|
+
overflow: hidden;
|
|
124
|
+
}
|
|
125
|
+
.lsc-root {
|
|
126
|
+
/* ---------------------------------
|
|
127
|
+
SCROLL ARROWS — RESTORED 1:1
|
|
128
|
+
---------------------------------- */
|
|
129
|
+
}
|
|
130
|
+
.lsc-root .lsc-arrow {
|
|
131
|
+
position: absolute;
|
|
132
|
+
top: 0;
|
|
133
|
+
bottom: 0;
|
|
134
|
+
width: 36px;
|
|
135
|
+
display: flex;
|
|
136
|
+
align-items: center;
|
|
137
|
+
justify-content: center;
|
|
138
|
+
font-size: 20px;
|
|
139
|
+
cursor: pointer;
|
|
140
|
+
user-select: none;
|
|
141
|
+
background: rgba(243, 237, 237, 0.12);
|
|
142
|
+
color: #fff;
|
|
143
|
+
border-radius: 8px;
|
|
144
|
+
z-index: 10;
|
|
145
|
+
transition: background 0.2s ease, transform 0.1s ease;
|
|
146
|
+
}
|
|
147
|
+
.lsc-root .lsc-arrow:hover {
|
|
148
|
+
background: rgba(0, 0, 0, 0.25);
|
|
149
|
+
transform: scale(1.05);
|
|
150
|
+
}
|
|
151
|
+
.lsc-root .lsc-arrow:active {
|
|
152
|
+
background: rgba(0, 0, 0, 0.35);
|
|
153
|
+
transform: scale(0.98);
|
|
154
|
+
}
|
|
155
|
+
.lsc-root.lsc-light .lsc-arrow {
|
|
156
|
+
background: rgba(0, 0, 0, 0.12);
|
|
157
|
+
}
|
|
158
|
+
.lsc-root.lsc-light .lsc-arrow:hover {
|
|
159
|
+
background: rgba(131, 119, 119, 0.12);
|
|
160
|
+
color: #393131;
|
|
161
|
+
}
|
|
162
|
+
.lsc-root .lsc-arrow-left {
|
|
163
|
+
left: 0;
|
|
164
|
+
}
|
|
165
|
+
.lsc-root .lsc-arrow-right {
|
|
166
|
+
right: 0;
|
|
167
|
+
}
|
package/dist/styles.scss
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
.lsc-root {
|
|
2
|
+
font-family: Inter, system-ui;
|
|
3
|
+
font-size: 13px;
|
|
4
|
+
z-index: 99999;
|
|
5
|
+
|
|
6
|
+
--bg: linear-gradient(180deg, #1b1f23, #111418);
|
|
7
|
+
--panel-bg: linear-gradient(180deg, #1e242a, #161b20);
|
|
8
|
+
--text: #9ca3af;
|
|
9
|
+
--subtext: #9ca3af;
|
|
10
|
+
--border: rgba(255, 255, 255, 0.08);
|
|
11
|
+
--hover: rgba(255, 255, 255, 0.08);
|
|
12
|
+
|
|
13
|
+
&.lsc-light {
|
|
14
|
+
--bg: linear-gradient(180deg, #ffffff, #f7f9fb);
|
|
15
|
+
--panel-bg: linear-gradient(180deg, #ffffff, #fbfdff);
|
|
16
|
+
--text: #0f1720;
|
|
17
|
+
--subtext: #556070;
|
|
18
|
+
--border: rgba(9, 30, 66, 0.06);
|
|
19
|
+
--hover: rgba(9, 30, 66, 0.06);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
.lsc-collapsed {
|
|
24
|
+
width: var(--lsc-collapsed);
|
|
25
|
+
height: var(--lsc-collapsed);
|
|
26
|
+
border-radius: 12px;
|
|
27
|
+
display: flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
justify-content: center;
|
|
30
|
+
background: var(--bg);
|
|
31
|
+
border: 1px solid var(--border);
|
|
32
|
+
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.4);
|
|
33
|
+
cursor: pointer;
|
|
34
|
+
overflow: hidden;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.lsc-thumb {
|
|
38
|
+
width: var(--lsc-thumb);
|
|
39
|
+
height: calc(var(--lsc-thumb) * 0.714);
|
|
40
|
+
border-radius: 8px;
|
|
41
|
+
object-fit: cover;
|
|
42
|
+
background: #0005;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.lsc-panel-wrap {
|
|
46
|
+
position: absolute;
|
|
47
|
+
top: 50%;
|
|
48
|
+
transform: translateY(-50%);
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
max-width: 100%;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.lsc-panel {
|
|
55
|
+
align-items: center;
|
|
56
|
+
padding: 8px;
|
|
57
|
+
gap: 8px;
|
|
58
|
+
border-radius: 12px;
|
|
59
|
+
background: var(--panel-bg);
|
|
60
|
+
border: 1px solid var(--border);
|
|
61
|
+
box-shadow: 0 12px 34px rgba(0, 0, 0, 0.4);
|
|
62
|
+
display: none;
|
|
63
|
+
transform: scale(0.97);
|
|
64
|
+
pointer-events: none;
|
|
65
|
+
transition: opacity 150ms ease, transform 200ms ease;
|
|
66
|
+
overflow: hidden;
|
|
67
|
+
|
|
68
|
+
&.is-open {
|
|
69
|
+
display: flex;
|
|
70
|
+
transform: scale(1);
|
|
71
|
+
pointer-events: auto;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.lsc-scroll-wrap {
|
|
76
|
+
position: relative;
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
gap: 4px;
|
|
80
|
+
overflow: hidden;
|
|
81
|
+
flex: 1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.lsc-tiles-wrap {
|
|
85
|
+
overflow-x: auto;
|
|
86
|
+
overflow-y: hidden;
|
|
87
|
+
scrollbar-width: none;
|
|
88
|
+
-ms-overflow-style: none;
|
|
89
|
+
flex: 1;
|
|
90
|
+
|
|
91
|
+
&::-webkit-scrollbar {
|
|
92
|
+
display: none;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.lsc-tiles {
|
|
97
|
+
display: flex;
|
|
98
|
+
gap: 8px;
|
|
99
|
+
flex-wrap: nowrap;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.lsc-tile {
|
|
103
|
+
display: flex;
|
|
104
|
+
flex-direction: column;
|
|
105
|
+
gap: 6px;
|
|
106
|
+
cursor: pointer;
|
|
107
|
+
width: var(--lsc-tile-width);
|
|
108
|
+
padding: 4px;
|
|
109
|
+
border-radius: 8px;
|
|
110
|
+
flex-shrink: 0;
|
|
111
|
+
|
|
112
|
+
&:hover {
|
|
113
|
+
background: var(--hover);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.lsc-t-thumb {
|
|
118
|
+
width: var(--lsc-tile-thumb);
|
|
119
|
+
height: calc(var(--lsc-tile-thumb) * 0.667);
|
|
120
|
+
border-radius: 8px;
|
|
121
|
+
object-fit: cover;
|
|
122
|
+
background: #0005;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.lsc-title {
|
|
126
|
+
font-weight: 600;
|
|
127
|
+
color: var(--text);
|
|
128
|
+
max-width: var(--lsc-tile-thumb);
|
|
129
|
+
white-space: nowrap;
|
|
130
|
+
overflow: hidden;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.lsc-desc {
|
|
134
|
+
font-size: 11px;
|
|
135
|
+
color: var(--subtext);
|
|
136
|
+
max-width: var(--lsc-tile-thumb);
|
|
137
|
+
white-space: nowrap;
|
|
138
|
+
overflow: hidden;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* ---------------------------------
|
|
142
|
+
SCROLL ARROWS — RESTORED 1:1
|
|
143
|
+
---------------------------------- */
|
|
144
|
+
|
|
145
|
+
.lsc-arrow {
|
|
146
|
+
position: absolute;
|
|
147
|
+
top: 0;
|
|
148
|
+
bottom: 0;
|
|
149
|
+
width: 36px;
|
|
150
|
+
|
|
151
|
+
display: flex;
|
|
152
|
+
align-items: center;
|
|
153
|
+
justify-content: center;
|
|
154
|
+
|
|
155
|
+
font-size: 20px;
|
|
156
|
+
cursor: pointer;
|
|
157
|
+
user-select: none;
|
|
158
|
+
|
|
159
|
+
background: rgba(243, 237, 237, 0.12);
|
|
160
|
+
color: #fff;
|
|
161
|
+
border-radius: 8px;
|
|
162
|
+
|
|
163
|
+
z-index: 10;
|
|
164
|
+
|
|
165
|
+
transition: background 0.2s ease, transform 0.1s ease;
|
|
166
|
+
|
|
167
|
+
&:hover {
|
|
168
|
+
background: rgba(0, 0, 0, 0.25);
|
|
169
|
+
transform: scale(1.05);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
&:active {
|
|
173
|
+
background: rgba(0, 0, 0, 0.35);
|
|
174
|
+
transform: scale(0.98);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
&.lsc-light {
|
|
179
|
+
.lsc-arrow {
|
|
180
|
+
background: rgba(0, 0, 0, 0.12);
|
|
181
|
+
&:hover {
|
|
182
|
+
background: rgba(131, 119, 119, 0.12);
|
|
183
|
+
color: #393131;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.lsc-arrow-left {
|
|
189
|
+
left: 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.lsc-arrow-right {
|
|
193
|
+
right: 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "layers-select-control",
|
|
3
|
+
"description": "Mimics Google layer select layers.",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build:ts": "tsc -p tsconfig.json",
|
|
9
|
+
"build:scss": "sass src:dist --no-source-map",
|
|
10
|
+
"copy:scss": "cpx \"src/**/*.scss\" dist/",
|
|
11
|
+
"build": "npm run build:ts && npm run build:scss && npm run copy:scss"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/react": "^19.1.16",
|
|
21
|
+
"cpx": "^1.5.0",
|
|
22
|
+
"sass": "^1.93.2",
|
|
23
|
+
"typescript": "^5.6.0"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"react": ">=18",
|
|
27
|
+
"react-dom": ">=18"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"url": "https://github.com/felipecarrillo100/layers-select-control"
|
|
31
|
+
}
|
|
32
|
+
}
|