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 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;
@@ -0,0 +1,2 @@
1
+ export { LayersSelectControl } from "./LayersSelectControl";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -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; } });
@@ -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
+ }
@@ -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
+ }