@scalable.software/pin 0.2.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/src/Index.ts ADDED
@@ -0,0 +1,25 @@
1
+ export { type Configuration } from "@scalable.software/component";
2
+
3
+ export {
4
+ Tag,
5
+ Attribute,
6
+ State,
7
+ Visible,
8
+ Status,
9
+ Operation,
10
+ Event,
11
+ Gesture,
12
+ } from "./Pin.meta.js";
13
+
14
+ export {
15
+ type Attributes,
16
+ type States,
17
+ type Visibility,
18
+ type Statuses,
19
+ type Operations,
20
+ type Events,
21
+ type Gestures,
22
+ type Handler,
23
+ } from "./Pin.meta.js";
24
+
25
+ export { Pin } from "./Pin.js";
@@ -0,0 +1,114 @@
1
+ /**
2
+ * @module Component
3
+ */
4
+
5
+ export const Tag = "pin-button" as const;
6
+
7
+ /**
8
+ * HTML Attributes available to set
9
+ * @category Metadata: State
10
+ * @enum
11
+ */
12
+ export const Attribute = {
13
+ VISIBLE: "visible",
14
+ STATUS: "status",
15
+ } as const;
16
+ /**
17
+ * HTML Attributes available to set
18
+ * @category Metadata: State
19
+ */
20
+ export type Attributes = typeof Attribute;
21
+
22
+ /**
23
+ * HTML Attributes available to set
24
+ * @category Metadata: State
25
+ * @enum
26
+ */
27
+ export const State = {
28
+ ...Attribute,
29
+ } as const;
30
+
31
+ /**
32
+ * HTML Attributes available to set
33
+ * @category Metadata: State
34
+ */
35
+ export type States = (typeof State)[keyof typeof State];
36
+
37
+ /**
38
+ * Visible state used to show or hide the component
39
+ * @category Metadata: State
40
+ * @enum
41
+ */
42
+ export const Visible = {
43
+ YES: "yes",
44
+ NO: "no",
45
+ } as const;
46
+ /**
47
+ * Visible state used to show or hide the component
48
+ * @category Metadata: State
49
+ */
50
+ export type Visibility = (typeof Visible)[keyof typeof Visible];
51
+
52
+ /**
53
+ * @category Metadata: State
54
+ * @enum
55
+ */
56
+ export const Status = {
57
+ PINNED: "pinned",
58
+ UNPINNED: "unpinned",
59
+ } as const;
60
+ /**
61
+ * @category Metadata: State
62
+ */
63
+ export type Statuses = (typeof Status)[keyof typeof Status];
64
+
65
+ /**
66
+ * @category Metadata: Operations
67
+ * @enum
68
+ */
69
+ export const Operation = {
70
+ HIDE: "hide",
71
+ SHOW: "show",
72
+ PIN: "pin",
73
+ UNPIN: "unpin",
74
+ TOGGLE: "toggle",
75
+ } as const;
76
+
77
+ /**
78
+ * @category Metadata: Operations
79
+ */
80
+ export type Operations = (typeof Operation)[keyof typeof Operation];
81
+
82
+ /**
83
+ * @category Metadata: Events
84
+ * @enum
85
+ */
86
+ export const Event = {
87
+ ON_HIDE: "onhide",
88
+ ON_SHOW: "onshow",
89
+ ON_PIN: "onpin",
90
+ ON_UNPIN: "onunpin",
91
+ ON: "on",
92
+ } as const;
93
+ /**
94
+ * @category Metadata: Events
95
+ */
96
+ export type Events = (typeof Event)[keyof typeof Event];
97
+
98
+ /**
99
+ * @category Metadata: Gesture
100
+ * @enum
101
+ */
102
+ export const Gesture = {
103
+ CLICK: "click",
104
+ } as const;
105
+ /**
106
+ * @category Metadata: Gesture
107
+ */
108
+ export type Gestures = (typeof Gesture)[keyof typeof Gesture];
109
+
110
+ /**
111
+ * Event handler signature
112
+ * @hidden
113
+ */
114
+ export type Handler = (...args: any[]) => void;
@@ -0,0 +1,122 @@
1
+ /* =======================
2
+ Host Element Styles
3
+ ======================= */
4
+ :host {
5
+ /* Light mode color variables */
6
+ --button-background-color: #ffffff;
7
+ --button-hover-background-color: #f3f3f3;
8
+ --button-active-background-color: #e1e1e1;
9
+ --button-border-color: #d1d1d1;
10
+ --button-border-hover-color: #a1a1a1;
11
+ --svg-fill-color: #5a5a5a;
12
+ --svg-hover-fill-color: #404040;
13
+ /* Shadow and radius */
14
+ --button-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
15
+ --button-radius: 8px;
16
+ /* Sizes and margins */
17
+ --icon-size: 40px;
18
+ --icon-margin: 4px;
19
+ --svg-size: 24px;
20
+ --svg-margin: 8px;
21
+ display: inline-block;
22
+ }
23
+ /* Dark Mode Variables */
24
+ @media (prefers-color-scheme: dark) {
25
+ :host {
26
+ /* Dark mode color variables */
27
+ --button-background-color: #1e1e1e;
28
+ --button-hover-background-color: #2a2a2a;
29
+ --button-active-background-color: #333333;
30
+ --button-border-color: #3c3c3c;
31
+ --button-border-hover-color: #4a4a4a;
32
+ --svg-fill-color: #cfcfcf;
33
+ --svg-hover-fill-color: #ffffff;
34
+ --button-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
35
+ }
36
+ }
37
+ /* =======================
38
+ Icon Styles
39
+ ======================= */
40
+ .icon {
41
+ position: relative;
42
+ margin: var(--icon-margin);
43
+ display: inline-flex;
44
+ align-items: center;
45
+ justify-content: center;
46
+ width: var(--icon-size);
47
+ height: var(--icon-size);
48
+ background-color: var(--button-background-color);
49
+ border: 1px solid var(--button-border-color);
50
+ border-radius: var(--button-radius);
51
+ box-shadow: var(--button-shadow);
52
+ cursor: pointer;
53
+ transition: background-color 0.2s, border-color 0.2s, box-shadow 0.2s;
54
+ }
55
+ /* Active state styles for the icon (when clicked or tapped) */
56
+ .icon:active {
57
+ background-color: var(--button-active-background-color);
58
+ border-color: var(--button-border-hover-color);
59
+ box-shadow: none;
60
+ }
61
+ /* =======================
62
+ Icon Hover Styles
63
+ ======================= */
64
+ @media (hover: hover) {
65
+ .icon:hover {
66
+ background-color: var(--button-hover-background-color);
67
+ border-color: var(--button-border-hover-color);
68
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
69
+ }
70
+ }
71
+ /* =======================
72
+ SVG Icon Styles
73
+ ======================= */
74
+ svg {
75
+ margin: var(--svg-margin);
76
+ width: var(--svg-size);
77
+ height: var(--svg-size);
78
+ fill: var(--svg-fill-color);
79
+ transition: fill 0.2s, opacity 0.2s;
80
+ }
81
+ /* Hover effect for the SVG icons */
82
+ @media (hover: hover) {
83
+ .icon:hover svg {
84
+ fill: var(--svg-hover-fill-color);
85
+ }
86
+ }
87
+
88
+ /* =======================
89
+ State Visibility
90
+ ======================= */
91
+ .pinned {
92
+ display: none;
93
+ }
94
+ .unpinned {
95
+ display: inline-block;
96
+ }
97
+ :host([visible="no"]) {
98
+ display: none;
99
+ }
100
+
101
+ /* =======================
102
+ State: Pinned
103
+ ======================= */
104
+ :host([status="pinned"]) .icon {
105
+ background-color: var(--button-active-background-color);
106
+ border-color: var(--button-border-hover-color);
107
+ }
108
+ :host([status="pinned"]) .pinned {
109
+ display: inline-block;
110
+ }
111
+ :host([status="pinned"]) .unpinned {
112
+ display: none;
113
+ }
114
+ /* =======================
115
+ State: Unpinned
116
+ ======================= */
117
+ :host([status="unpinned"]) .pinned {
118
+ display: none;
119
+ }
120
+ :host([status="unpinned"]) .unpinned {
121
+ display: inline-block;
122
+ }
@@ -0,0 +1,12 @@
1
+ <template id="pin-button">
2
+ <div class="icon">
3
+ <svg class="pinned" height="24px" width="24px" viewBox="0 0 20 20" fill="#212121">
4
+ <path
5
+ d="M2.85355 2.14645C2.65829 1.95118 2.34171 1.95118 2.14645 2.14645C1.95118 2.34171 1.95118 2.65829 2.14645 2.85355L6.896 7.60309L4.01834 8.75415C3.35177 9.02078 3.17498 9.88209 3.68262 10.3897L6.29289 13L3 16.2929V17H3.70711L7 13.7071L9.61027 16.3174C10.1179 16.825 10.9792 16.6482 11.2459 15.9817L12.3969 13.104L17.1464 17.8536C17.3417 18.0488 17.6583 18.0488 17.8536 17.8536C18.0488 17.6583 18.0488 17.3417 17.8536 17.1464L2.85355 2.14645ZM11.6276 12.3347L10.3174 15.6103L4.38973 9.68263L7.66531 8.3724L11.6276 12.3347ZM12.9565 10.7127C12.9294 10.7263 12.9026 10.7403 12.8761 10.7548L13.6202 11.4989L16.8622 9.87793C18.0832 9.26743 18.3473 7.64015 17.382 6.67486L13.3251 2.61804C12.3599 1.65275 10.7326 1.91683 10.1221 3.13783L8.5011 6.37977L9.24523 7.1239C9.25971 7.09739 9.27373 7.07059 9.28728 7.04349L11.0165 3.58504C11.3218 2.97454 12.1354 2.8425 12.618 3.32514L16.6749 7.38197C17.1575 7.86461 17.0255 8.67826 16.415 8.98351L12.9565 10.7127Z" />
6
+ </svg>
7
+ <svg class="unpinned" height="24px" width="24px" viewBox="0 0 20 20" fill="#212121">
8
+ <path
9
+ d="M10.1221 3.13782C10.7326 1.91683 12.3599 1.65275 13.3251 2.61804L17.382 6.67486C18.3472 7.64015 18.0832 9.26743 16.8622 9.87793L13.4037 11.6072C13.0751 11.7715 12.8183 12.0506 12.6818 12.3917L11.2459 15.9817C10.9792 16.6482 10.1179 16.825 9.61027 16.3174L7 13.7071L3.70711 17H3V16.2929L6.29289 13L3.68262 10.3897C3.17498 9.88209 3.35177 9.02078 4.01834 8.75415L7.60829 7.31817C7.94939 7.18173 8.22855 6.92486 8.39285 6.59628L10.1221 3.13782ZM12.618 3.32514C12.1354 2.8425 11.3217 2.97454 11.0165 3.58504L9.28727 7.04349C9.01345 7.59113 8.54818 8.01925 7.97968 8.24665L4.38973 9.68263L10.3174 15.6103L11.7534 12.0203C11.9808 11.4518 12.4089 10.9866 12.9565 10.7127L16.415 8.9835C17.0255 8.67826 17.1575 7.86461 16.6749 7.38197L12.618 3.32514Z" />
10
+ </svg>
11
+ </div>
12
+ </template>
package/src/Pin.ts ADDED
@@ -0,0 +1,329 @@
1
+ /**
2
+ * @module Component
3
+ */
4
+ import { Component, Template } from "@scalable.software/component";
5
+ import { type Configuration } from "@scalable.software/component";
6
+
7
+ import { Tag, Attribute, Visible, Status, Event, Gesture } from "./Pin.meta.js";
8
+ import {
9
+ type Attributes,
10
+ type Visibility,
11
+ type Statuses,
12
+ type Events,
13
+ type Handler,
14
+ } from "./Pin.meta.js";
15
+
16
+
17
+
18
+ /**
19
+ * Configuration required for components with custom layout and style
20
+ * @category Configuration
21
+ */
22
+ export const configuration: Configuration = {
23
+ url: import.meta.url,
24
+ template: {
25
+ id: Tag,
26
+ },
27
+ css: {
28
+ name: "Pin.style.css",
29
+ },
30
+ } as const;
31
+
32
+ /**
33
+ * A pin button that can be:
34
+ * 1. pinned and unpinned
35
+ * 2. hidden and shown
36
+ * @category Components
37
+ */
38
+ export class Pin extends Component {
39
+ /**
40
+ * The tag name of the component
41
+ * @category Configuration
42
+ */
43
+ public static get Tag() {
44
+ return Tag;
45
+ }
46
+
47
+ /**
48
+ * Only attributes defined the Attributes object will be observed in DOM
49
+ * @category Configuration
50
+ */
51
+ public static get Attributes(): Attributes {
52
+ return Attribute;
53
+ }
54
+
55
+ /**
56
+ * Wait for the component to be defined before returning a collection of the component in the DOM
57
+ * @category Utilities
58
+ */
59
+ public static get = async () =>
60
+ (await customElements.whenDefined(Pin.Tag)) &&
61
+ (Array.from(document.querySelectorAll(Pin.Tag)) as Pin[]);
62
+
63
+ /**
64
+ * Helper function to load the component template into DOM
65
+ * @category Utilities
66
+ */
67
+ public static Template = new Template(import.meta.url);
68
+
69
+ /**
70
+ * Cache element references to improve performance
71
+ * @category State
72
+ * @hidden
73
+ */
74
+ protected elements: { icon: HTMLDivElement | null } = { icon: null };
75
+
76
+ /**
77
+ * Internal Visibility state of the component
78
+ * @category State
79
+ * @default Visible.YES
80
+ * @hidden
81
+ */
82
+ private _visible: Visibility = Visible.YES;
83
+
84
+ /**
85
+ * Internal Status state of the component
86
+ * @category State
87
+ * @default
88
+ * @hidden
89
+ */
90
+ private _status: Statuses = Status.UNPINNED;
91
+
92
+ /**
93
+ * on triggered when any event is triggered
94
+ * @category Events
95
+ * @hidden
96
+ */
97
+ private _on: Handler = null;
98
+
99
+ /**
100
+ * onhide triggered when pin visibility changes to hidden
101
+ * @category Events
102
+ * @hidden
103
+ */
104
+ private _onhide: Handler = null;
105
+
106
+ /**
107
+ * onshow triggered when pin visibility changes to visible
108
+ * @category Events
109
+ * @hidden
110
+ */
111
+ private _onshow: Handler = null;
112
+
113
+ /**
114
+ * onpin triggered when pin status changes to pinned
115
+ * @category Events
116
+ * @hidden
117
+ */
118
+ private _onpin: Handler = null;
119
+
120
+ /**
121
+ * onunpin triggered when pin state changes to unpinned
122
+ * @category Events
123
+ * @hidden
124
+ */
125
+ private _onunpin: Handler = null;
126
+
127
+ constructor() {
128
+ super(configuration);
129
+ }
130
+
131
+ /**
132
+ * Get and Sets the visibility of the pin button
133
+ * @category State
134
+ */
135
+ public get visible(): Visibility {
136
+ return this.hasAttribute(Attribute.VISIBLE)
137
+ ? (this.getAttribute(Attribute.VISIBLE) as Visibility)
138
+ : this._visible;
139
+ }
140
+ public set visible(visible: Visibility) {
141
+ visible = visible || Visible.YES;
142
+ if (this._visible !== visible) {
143
+ this._visible = visible;
144
+ visible == Visible.YES && this.removeAttribute(Attribute.VISIBLE);
145
+ visible == Visible.NO && this.setAttribute(Attribute.VISIBLE, visible);
146
+
147
+ visible == Visible.NO &&
148
+ this._dispatchEvent(Event.ON_HIDE, { detail: { visible } });
149
+ visible == Visible.YES &&
150
+ this._dispatchEvent(Event.ON_SHOW, { detail: { visible } });
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Get and Sets the status of the pin button
156
+ * @category State
157
+ */
158
+ public get status(): Statuses {
159
+ return this.hasAttribute(Attribute.STATUS)
160
+ ? (this.getAttribute(Attribute.STATUS) as Statuses)
161
+ : this._status;
162
+ }
163
+ public set status(status: Statuses) {
164
+ if (this._status !== status) {
165
+ this._status = status;
166
+ this.setAttribute(Attribute.STATUS, status);
167
+
168
+ status == Status.PINNED &&
169
+ this._dispatchEvent(Event.ON_PIN, { detail: { status } });
170
+ status == Status.UNPINNED &&
171
+ this._dispatchEvent(Event.ON_UNPIN, { detail: { status } });
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Triggered on any event
177
+ * @event
178
+ * @category Events
179
+ */
180
+ public set on(handler: Handler) {
181
+ Object
182
+ .values(Event)
183
+ .filter((event): event is Events => event !== Event.ON)
184
+ .forEach((event: Events) => {
185
+ this._on && this.removeEventListener(event, this._on);
186
+ this._on = handler;
187
+ this._on && this.addEventListener(event, this._on);
188
+ });
189
+ }
190
+
191
+ /**
192
+ * Triggered via `.hide()`
193
+ * @event
194
+ * @category Events
195
+ */
196
+ public set onhide(handler: Handler) {
197
+ this._onhide && this.removeEventListener(Event.ON_HIDE, this._onhide);
198
+ this._onhide = handler;
199
+ this._onhide && this.addEventListener(Event.ON_HIDE, this._onhide);
200
+ }
201
+
202
+ /**
203
+ * Triggered via `.show()`
204
+ * @event
205
+ * @category Events
206
+ */
207
+ public set onshow(handler: Handler) {
208
+ this._onshow && this.removeEventListener(Event.ON_SHOW, this._onshow);
209
+ this._onshow = handler;
210
+ this._onshow && this.addEventListener(Event.ON_SHOW, this._onshow);
211
+ }
212
+
213
+ /**
214
+ * Triggered via `.pin()`
215
+ * @event
216
+ * @category Events
217
+ */
218
+ public set onpin(handler: Handler) {
219
+ this._onpin && this.removeEventListener(Event.ON_PIN, this._onpin);
220
+ this._onpin = handler;
221
+ this._onpin && this.addEventListener(Event.ON_PIN, this._onpin);
222
+ }
223
+
224
+ /**
225
+ * Triggered via `.unpin()`
226
+ * @event
227
+ * @category Events
228
+ */
229
+ public set onunpin(handler: Handler) {
230
+ this._onunpin && this.removeEventListener(Event.ON_UNPIN, this._onunpin);
231
+ this._onunpin = handler;
232
+ this._onunpin && this.addEventListener(Event.ON_UNPIN, this._onunpin);
233
+ }
234
+
235
+ /**
236
+ * Hide the pin button when it is visible
237
+ * @category Operations
238
+ */
239
+ public hide = () => (this.visible = Visible.NO);
240
+
241
+ /**
242
+ * Show the pin button when it is hidden
243
+ * @category Operations
244
+ */
245
+ public show = () => (this.visible = Visible.YES);
246
+
247
+ /**
248
+ * Pin the pin button when it is unpinned
249
+ * @category Operations
250
+ */
251
+ public pin = () => (this.status = Status.PINNED);
252
+
253
+ /**
254
+ * Unpin the pin button when it is pinned
255
+ * @category Operations
256
+ */
257
+ public unpin = () => (this.status = Status.UNPINNED);
258
+
259
+ /**
260
+ * Toggle the pin button between pinned and unpinned
261
+ * @category Operations
262
+ */
263
+ public toggle = () =>
264
+ (this.status =
265
+ this.status === Status.PINNED ? Status.UNPINNED : Status.PINNED);
266
+
267
+ /**
268
+ * List operations to perform for selected attributes being observed in the DOM.
269
+ * @category Configuration
270
+ * @hidden
271
+ */
272
+ protected _attributeHandlers = {
273
+ [Attribute.VISIBLE]: (value) => (this.visible = value),
274
+ [Attribute.STATUS]: (value) => (this.status = value),
275
+ };
276
+
277
+ /**
278
+ * Initialize component attributes with default values
279
+ * @category Configuration
280
+ * @hidden
281
+ */
282
+ protected _initialize = () => this._initializeState();
283
+
284
+ /**
285
+ * Cache element references to improve performance
286
+ * @category Configuration
287
+ * @hidden
288
+ */
289
+ protected _cacheElements = () => this._cacheIcon();
290
+
291
+ /**
292
+ * Called by the connectedCallback prototypical method
293
+ * @category Configuration
294
+ * @hidden
295
+ */
296
+ protected _addEventListeners = () =>
297
+ this.elements.icon.addEventListener(Gesture.CLICK, this._handleClick);
298
+
299
+ /**
300
+ * Called by the disconnectedCallback prototypical method
301
+ * @category Configuration
302
+ * @hidden
303
+ */
304
+ protected _removeEventListeners = () =>
305
+ this.elements.icon.removeEventListener(Gesture.CLICK, this._handleClick);
306
+
307
+ /**
308
+ * Initialize component state with default values
309
+ * @category State
310
+ * @hidden
311
+ */
312
+ private _initializeState = () =>
313
+ this.setAttribute(Attribute.STATUS, this._status);
314
+
315
+ /**
316
+ * Cache icon element reference to improve performance
317
+ * @category State
318
+ * @hidden
319
+ */
320
+ private _cacheIcon = () =>
321
+ (this.elements.icon = this.root.querySelector(".icon"));
322
+
323
+ /**
324
+ * Handles the click event
325
+ * @category Gesture
326
+ * @hidden
327
+ */
328
+ private _handleClick = (event: MouseEvent | TouchEvent) => this.toggle();
329
+ }