@esavoretti/flexmenu 1.0.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/LICENSE +21 -0
- package/README.md +86 -0
- package/favicon.ico +0 -0
- package/flexmenu.js +664 -0
- package/index.html +54 -0
- package/index.js +23 -0
- package/package.json +17 -0
- package/runserver.py +25 -0
- package/styles.css +122 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Domingo Ernesto Savoretti
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# flexmenu
|
|
2
|
+
|
|
3
|
+
Menu web component with built-in flexibility to accommodate itself to mobile screens
|
|
4
|
+
|
|
5
|
+
**Provided Components**
|
|
6
|
+
|
|
7
|
+
* **`<color-label>`: Base presentation element utilizing the styling mixin pipeline.**
|
|
8
|
+
* **`<menu-item>`: Clickable, navigation-ready component designed to live inside menus.**
|
|
9
|
+
* **`<menu-list>`: Container element.** *Accepts `<menu-item>`, `<menu-list>` and `<span>` (as separator) elements.*
|
|
10
|
+
* **`<hamburger-button>`: Responsive trigger element.**
|
|
11
|
+
* **`<flex-menu>`: The top-level responsive layout framework**.** **: Base presentation element utilizing the styling mixin pipeline.**
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
Global Styling System
|
|
16
|
+
|
|
17
|
+
All elements derived from the internal `ColorMixin` pipeline feature fully synchronized CSS custom properties, HTML attributes, and JavaScript DOM object properties.
|
|
18
|
+
|
|
19
|
+
Custom Styling Attributes / CSS Variables
|
|
20
|
+
|
|
21
|
+
Pass these as HTML attributes or set them in JS. The component dynamically translates them into matching CSS variables behind a Shadow DOM boundary.
|
|
22
|
+
|
|
23
|
+
| HTML Attribute | DOM Property | Default Value | Description |
|
|
24
|
+
| -------------- | ------------ | ---------------------- | ------------------------------- |
|
|
25
|
+
| `fg-color` | `fgColor` | `aliceblue` | Text/Foreground color |
|
|
26
|
+
| `bg-color` | `bgColor` | `steelblue` | Default state background color |
|
|
27
|
+
| `sel-color` | `selColor` | `rgb(190, 252, 190)` | Selected state background color |
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
Component Reference
|
|
32
|
+
|
|
33
|
+
1. `<color-label>`
|
|
34
|
+
|
|
35
|
+
A basic structural container that exposes the fundamental color theme mapping.
|
|
36
|
+
|
|
37
|
+
html
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
<color-label fg-color="white" bg-color="#333" sel-color="coral">
|
|
41
|
+
Label Content
|
|
42
|
+
</color-label>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Properties & Methods
|
|
46
|
+
|
|
47
|
+
* **`selected`** *(Boolean)* **: Gets or sets the visual active selection state.**
|
|
48
|
+
* **`select()`** **: Explicitly forces the component into the selected state.**
|
|
49
|
+
* **`unselect()`** **: Explicitly removes the selected state.**
|
|
50
|
+
* **`toggleSelected()`** **: Reverses the current selection state.**
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
2. `<menu-item>`
|
|
55
|
+
|
|
56
|
+
Extends `<color-label>` to provide navigation handling, interaction patterns, and parent-sibling awareness.
|
|
57
|
+
|
|
58
|
+
html
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
<menu-item href="#gallery" use-default-handler fg-color="yellow">
|
|
62
|
+
Gallery
|
|
63
|
+
</menu-item>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Unique Attributes & Properties
|
|
67
|
+
|
|
68
|
+
* **`href`** / `href` *(String, default: `'#'`)* **: Destination anchor link URL target.**
|
|
69
|
+
* **`use-default-handler`** / `useDefaultHandler` *(Boolean, default: `false`)* **: When active, automatically transitions active sibling states and routes the page location automatically upon user click.**
|
|
70
|
+
|
|
71
|
+
Methods
|
|
72
|
+
|
|
73
|
+
* **`defaultHandler(force = false)`** **: Processes mutual exclusivity logic. Clears target states from matching sibling **`<menu-item>` elements, selects itself, and pushes changes to window location tracking.
|
|
74
|
+
* **`unselectSiblings()`** **: Manually queries parent nodes to strip **`.selected` classes from competing menu items.
|
|
75
|
+
|
|
76
|
+
Custom Events Emitted
|
|
77
|
+
|
|
78
|
+
* **`menu-item-click`** **: Fires on selection interaction. Bubbles outside the shadow root safely.**
|
|
79
|
+
* **`event.detail` structure** **:**
|
|
80
|
+
javascript
|
|
81
|
+
|
|
82
|
+
``{ target: HTMLElement, // The specific menu-item instance type: 'click', srcEvent: MouseEvent // Native mouse click details }``
|
|
83
|
+
|
|
84
|
+
## Demo
|
|
85
|
+
|
|
86
|
+
[Assorted menus](https://sandy98.github.io/flexmenu)
|
package/favicon.ico
ADDED
|
Binary file
|
package/flexmenu.js
ADDED
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
|
|
2
|
+
const baseStyle = `
|
|
3
|
+
<style>
|
|
4
|
+
:host {
|
|
5
|
+
display: inline-flex;
|
|
6
|
+
width: 10rem;
|
|
7
|
+
max-width: 15rem;
|
|
8
|
+
height: 1.5rem;
|
|
9
|
+
min-height: 1.5rem;
|
|
10
|
+
position: relative;
|
|
11
|
+
z-index: 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* The container takes care of all styling */
|
|
15
|
+
.wrapper {
|
|
16
|
+
|
|
17
|
+
/* Reads the value from the host style attribute seamlessly */
|
|
18
|
+
color: var(--fg-color, aliceblue);
|
|
19
|
+
background-color: var(--bg-color, steelblue);
|
|
20
|
+
position: relative;
|
|
21
|
+
display: flex;
|
|
22
|
+
width: 100%;
|
|
23
|
+
height: 100%;
|
|
24
|
+
justify-content: center;
|
|
25
|
+
align-items: center;
|
|
26
|
+
/* gap: 0.5rem; */
|
|
27
|
+
user-select: none;
|
|
28
|
+
touch-action: none;
|
|
29
|
+
transition: background-color 0.3s ease, color 0.3s ease;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* When the host gets the class, change the inner wrapper's background */
|
|
33
|
+
:host(.selected) .wrapper {
|
|
34
|
+
background-color: var(--sel-color, rgb(190, 252, 190));
|
|
35
|
+
}
|
|
36
|
+
</style>
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
const baseHtml = (slotContent = 'DIV') => `
|
|
40
|
+
<div class="wrapper">
|
|
41
|
+
<slot>${slotContent}</slot>
|
|
42
|
+
</div>
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
//
|
|
46
|
+
|
|
47
|
+
const ColorMixin = function(elementType = HTMLElement) {
|
|
48
|
+
const customClass = class extends elementType {
|
|
49
|
+
constructor() {
|
|
50
|
+
super();
|
|
51
|
+
this.attachShadow({mode: 'open'});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static get observedAttributes() {
|
|
55
|
+
return ['fg-color', 'bg-color', 'sel-color'];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
59
|
+
console.log(`Attribute ${name} has changed from ${oldValue} to ${newValue}`);
|
|
60
|
+
this.style.setProperty(`--${name}`, newValue);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
connectedCallback() {
|
|
64
|
+
this.shadowRoot.innerHTML = `${baseStyle}${baseHtml(this.tagName)}`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getAttribute(name) {
|
|
68
|
+
switch(name) {
|
|
69
|
+
case 'fg-color':
|
|
70
|
+
return this.style.getPropertyValue('--fg-color') || 'aliceblue';
|
|
71
|
+
break;
|
|
72
|
+
case 'bg-color':
|
|
73
|
+
return this.style.getPropertyValue('--bg-color') || 'steelblue';
|
|
74
|
+
break;
|
|
75
|
+
case 'sel-color':
|
|
76
|
+
return this.style.getPropertyValue('--sel-color') || 'rgb(190, 252, 190)';
|
|
77
|
+
break;
|
|
78
|
+
default:
|
|
79
|
+
return super.getAttribute(name);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
toggleSelected() {
|
|
84
|
+
this.classList.toggle('selected');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
select() {
|
|
88
|
+
this.classList.toggle('selected', true);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
unselect() {
|
|
92
|
+
this.classList.toggle('selected', false);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
get selected() { return this.classList.contains('selected');}
|
|
96
|
+
set selected(bselected) {
|
|
97
|
+
if (!!bselected) {
|
|
98
|
+
this.select();
|
|
99
|
+
} else {
|
|
100
|
+
this.unselect();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
get fgColor() { return this.getAttribute('fg-color');}
|
|
105
|
+
set fgColor(newColor) {this.setAttribute('fg-color', newColor);}
|
|
106
|
+
// set fgColor(newColor) {this.style.setProperty('--fg-color', newColor);}
|
|
107
|
+
|
|
108
|
+
get bgColor() { return this.getAttribute('bg-color');}
|
|
109
|
+
set bgColor(newColor) {this.setAttribute('bg-color', newColor);}
|
|
110
|
+
// set bgColor(newColor) {this.style.setProperty('--bg-color', newColor);}
|
|
111
|
+
|
|
112
|
+
get selColor() { return this.getAttribute('sel-color');}
|
|
113
|
+
set selColor(newColor) {this.setAttribute('sel-color', newColor);}
|
|
114
|
+
// set selColor(newColor) {this.style.setProperty('--sel-color', newColor);}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return customClass;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// const ColorLabel = ColorMixin();
|
|
121
|
+
export class ColorLabel extends ColorMixin() {}
|
|
122
|
+
|
|
123
|
+
window.customElements.define('color-label', ColorLabel);
|
|
124
|
+
|
|
125
|
+
//
|
|
126
|
+
|
|
127
|
+
const menuItemStyle = `
|
|
128
|
+
<style>
|
|
129
|
+
:host {
|
|
130
|
+
cursor: pointer;
|
|
131
|
+
}
|
|
132
|
+
.wrapper {
|
|
133
|
+
padding-top: 3px;
|
|
134
|
+
padding-bottom: 3px;
|
|
135
|
+
}
|
|
136
|
+
.wrapper:hover {
|
|
137
|
+
color: var(--bg-color, steelblue);
|
|
138
|
+
background-color: var(--fg-color, aliceblue);
|
|
139
|
+
border: dotted 1px var(--bg-color, steelblue);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
:host(.selected) .wrapper {
|
|
143
|
+
color: var(--bg-color, steelblue);
|
|
144
|
+
background-color: var(--sel-color, rgb(190, 252, 190));
|
|
145
|
+
border: dotted 1px var(--bg-color, steelblue);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
:host(.selected) .wrapper:hover {
|
|
149
|
+
color: var(--sel-color, rgb(190, 252, 190));
|
|
150
|
+
background-color: var(--bg-color, steelblue);
|
|
151
|
+
border: dotted 1px var(--sel-color, rgb(190, 252, 190));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
</style>
|
|
155
|
+
`;
|
|
156
|
+
|
|
157
|
+
export class MenuItem extends ColorMixin() {
|
|
158
|
+
|
|
159
|
+
constructor() {
|
|
160
|
+
super();
|
|
161
|
+
this._href = '#';
|
|
162
|
+
this._use_default_handler = false;
|
|
163
|
+
this.boundClick = this.onClick.bind(this);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
static get observedAttributes() {
|
|
167
|
+
return ["href", "use-default-handler", ...super.observedAttributes];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
171
|
+
switch (name) {
|
|
172
|
+
case "href":
|
|
173
|
+
this.href = newValue;
|
|
174
|
+
break;
|
|
175
|
+
case "use-default-handler":
|
|
176
|
+
let realvalue = newValue === '' || !!newValue ? true : false;
|
|
177
|
+
this.useDefaultHandler = realvalue;
|
|
178
|
+
break;
|
|
179
|
+
default:
|
|
180
|
+
super.attributeChangedCallback(name, oldValue, newValue);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
connectedCallback() {
|
|
185
|
+
this.shadowRoot.innerHTML = `${baseStyle}${menuItemStyle}${baseHtml(this.tagName)}`;
|
|
186
|
+
this.addEventListener('click', this.boundClick);
|
|
187
|
+
if (location.href.endsWith(this.href) && this.useDefaultHandler) this.select();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
disconnectedCallback() {
|
|
191
|
+
this.removeEventListener('click', this.boundClick);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
getAttribute(name) {
|
|
195
|
+
switch(name) {
|
|
196
|
+
case 'href':
|
|
197
|
+
return this.href;
|
|
198
|
+
break;
|
|
199
|
+
case 'use-default-handler':
|
|
200
|
+
return this.useDefaultHandler;
|
|
201
|
+
break;
|
|
202
|
+
default:
|
|
203
|
+
return super.getAttribute(name);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
removeAttribute(name) {
|
|
208
|
+
if (name === 'use-default-handler') {
|
|
209
|
+
this.useDefaultHandler = false;
|
|
210
|
+
} else {
|
|
211
|
+
super.removeAttribute(name);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
get href() {return this._href;}
|
|
216
|
+
set href(newHref) {this._href = newHref;}
|
|
217
|
+
|
|
218
|
+
get useDefaultHandler() {return this._use_default_handler;}
|
|
219
|
+
set useDefaultHandler(newDefault) {this._use_default_handler = !!newDefault;}
|
|
220
|
+
|
|
221
|
+
unselectSiblings() {
|
|
222
|
+
const parent = this.parentElement;
|
|
223
|
+
const siblings = parent.querySelectorAll('menu-item');
|
|
224
|
+
siblings.forEach(menuitem => { if (menuitem !== this) menuitem.unselect(); });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
defaultHandler(force = false) {
|
|
228
|
+
if (!force && !this.useDefaultHandler) return;
|
|
229
|
+
this.unselectSiblings();
|
|
230
|
+
this.select();
|
|
231
|
+
location.href = this.href;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
onClick(evt) {
|
|
235
|
+
// console.log("Click on menuItem");
|
|
236
|
+
const options = {detail: {target: this, type: 'click', srcEvent: evt}, composed: true, bubbles: true};
|
|
237
|
+
const clickEvent = new CustomEvent('menu-item-click', options);
|
|
238
|
+
this.dispatchEvent(clickEvent);
|
|
239
|
+
this.defaultHandler();
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
window.customElements.define('menu-item', MenuItem);
|
|
245
|
+
|
|
246
|
+
//
|
|
247
|
+
|
|
248
|
+
const menuListStyle = `
|
|
249
|
+
<style>
|
|
250
|
+
:host {
|
|
251
|
+
display: flex;
|
|
252
|
+
width: 10rem;
|
|
253
|
+
max-width: 15rem;
|
|
254
|
+
margin-top: 0;
|
|
255
|
+
margin-left: 0;
|
|
256
|
+
position: relative;
|
|
257
|
+
z-index: 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/* The container takes care of all styling */
|
|
261
|
+
.wrapper {
|
|
262
|
+
|
|
263
|
+
/* Reads the value from the host style attribute seamlessly */
|
|
264
|
+
color: var(--fg-color, aliceblue);
|
|
265
|
+
background-color: var(--bg-color, steelblue);
|
|
266
|
+
position: relative;
|
|
267
|
+
display: flex;
|
|
268
|
+
flex-direction: column;
|
|
269
|
+
width: 100%;
|
|
270
|
+
height: 100%;
|
|
271
|
+
justify-content: center;
|
|
272
|
+
align-items: center;
|
|
273
|
+
gap: 0.5rem;
|
|
274
|
+
user-select: none;
|
|
275
|
+
z-index: 1;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.wrapper color-label {
|
|
279
|
+
cursor: pointer;
|
|
280
|
+
border-bottom: solid 1px var(--fg-color, aliceblue);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.wrapper color-label::after {
|
|
284
|
+
content: "\\25BC";
|
|
285
|
+
font-size: 0.75rem;
|
|
286
|
+
transition: transform 0.3s ease;
|
|
287
|
+
display: inline-flex;
|
|
288
|
+
flex-direction: row;
|
|
289
|
+
justify-content: center;
|
|
290
|
+
align-items: center;
|
|
291
|
+
padding: 3px;
|
|
292
|
+
color: var(--fg-color, aliceblue);
|
|
293
|
+
background-color: transparent;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.wrapper:hover color-label::after {
|
|
297
|
+
transform: rotate(180deg);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.item-container {
|
|
301
|
+
margin: 0;
|
|
302
|
+
position: absolute;
|
|
303
|
+
/*top: calc(1.5rem + 1px); */
|
|
304
|
+
top: 0;
|
|
305
|
+
left: -100vw;
|
|
306
|
+
display: flex;
|
|
307
|
+
flex-direction: column;
|
|
308
|
+
justify-content: stretch;
|
|
309
|
+
align-items: center;
|
|
310
|
+
width: 10rem;
|
|
311
|
+
max-width: 15rem;
|
|
312
|
+
gap: 0;
|
|
313
|
+
transition: left 0.3s fade-in-out;
|
|
314
|
+
z-index: 2;
|
|
315
|
+
}
|
|
316
|
+
.wrapper:hover .item-container {
|
|
317
|
+
top: 0;
|
|
318
|
+
left: 99%;
|
|
319
|
+
z-index: 10;
|
|
320
|
+
@media screen and (orientation: landscape) {
|
|
321
|
+
/* top: calc(1.5rem + 1px); */
|
|
322
|
+
top: calc(1.5rem + 1px);
|
|
323
|
+
left: 0;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
::slotted(*) {
|
|
327
|
+
display: none;
|
|
328
|
+
}
|
|
329
|
+
::slotted(menu-item), ::slotted(menu-list) {
|
|
330
|
+
display: inherit;
|
|
331
|
+
padding: 5px;
|
|
332
|
+
}
|
|
333
|
+
::slotted(span) {
|
|
334
|
+
content: "";
|
|
335
|
+
display: inline-block;
|
|
336
|
+
background-color: var(--fg-color);
|
|
337
|
+
color: var(--bg-color);
|
|
338
|
+
width: 100%;
|
|
339
|
+
min-width: 100%;
|
|
340
|
+
height: 1px;
|
|
341
|
+
max-height: 1px;
|
|
342
|
+
min-height: 1px;
|
|
343
|
+
}
|
|
344
|
+
</style>
|
|
345
|
+
`;
|
|
346
|
+
|
|
347
|
+
function menuListHtml(menuTitle = "MENU-LIST") {
|
|
348
|
+
return `
|
|
349
|
+
<div class="wrapper">
|
|
350
|
+
<color-label>${menuTitle}</color-label>
|
|
351
|
+
<div class="item-container">
|
|
352
|
+
<slot></slot>
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export class MenuList extends ColorMixin() {
|
|
360
|
+
constructor() {
|
|
361
|
+
super();
|
|
362
|
+
this._menuTitle = this.tagName;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
connectedCallback() {
|
|
366
|
+
this.shadowRoot.innerHTML = `${menuListStyle}${menuListHtml(this.menuTitle)}`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
static get observedAttributes() {
|
|
370
|
+
return ['menu-title', ...super.observedAttributes];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
374
|
+
if (name === 'menu-title') {
|
|
375
|
+
this.menuTitle = newValue;
|
|
376
|
+
} else {
|
|
377
|
+
super.attributeChangedCallback(name, oldValue, newValue);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
getAttribute(attrName) {
|
|
382
|
+
if (attrName === 'menu-title') return this.menuTitle;
|
|
383
|
+
return super.getAttribute(attrName);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
get menuTitle() { return this._menuTitle};
|
|
387
|
+
set menuTitle(newTitle) {
|
|
388
|
+
this._menuTitle = newTitle;
|
|
389
|
+
this.showTitle();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
showTitle() {
|
|
393
|
+
const label = this.shadowRoot.querySelector('color-label');
|
|
394
|
+
if (!label) return;
|
|
395
|
+
label.textContent = this.menuTitle;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
window.customElements.define('menu-list', MenuList);
|
|
400
|
+
|
|
401
|
+
//
|
|
402
|
+
|
|
403
|
+
const mainMenuStyle = `
|
|
404
|
+
<style>
|
|
405
|
+
.item-container {
|
|
406
|
+
display: flex;
|
|
407
|
+
flex-direction: column;
|
|
408
|
+
gap: 0;
|
|
409
|
+
justify-content: flex-start;
|
|
410
|
+
align-items: left;
|
|
411
|
+
width: fit-content;
|
|
412
|
+
@media screen and (orientation: landscape) {
|
|
413
|
+
flex-direction: row;
|
|
414
|
+
align-items: center;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
</style>
|
|
418
|
+
`;
|
|
419
|
+
|
|
420
|
+
function mainMenuHtml() {
|
|
421
|
+
return `
|
|
422
|
+
<div class="wrapper">
|
|
423
|
+
<nav class="item-container">
|
|
424
|
+
<slot></slot>
|
|
425
|
+
</nav>
|
|
426
|
+
</div>
|
|
427
|
+
`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export class MainMenu extends MenuList {
|
|
431
|
+
connectedCallback() {
|
|
432
|
+
// this.shadowRoot.innerHTML = `${menuListStyle}${mainMenuStyle}${mainMenuHtml()}`;
|
|
433
|
+
this.shadowRoot.innerHTML = `${mainMenuStyle}${mainMenuHtml()}`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
getAttribute(attrName) {
|
|
437
|
+
if (attrName === 'menu-title') return this.menuTitle;
|
|
438
|
+
return super.getAttribute(attrName);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
get menuTitle() { return ""};
|
|
442
|
+
set menuTitle(newTitle) {
|
|
443
|
+
// No title for main menu
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
showTitle() {
|
|
447
|
+
// No title for main menu
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
window.customElements.define('main-menu', MainMenu);
|
|
452
|
+
|
|
453
|
+
//
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
const hamburgerStyle = `
|
|
457
|
+
<style>
|
|
458
|
+
:host {
|
|
459
|
+
width: calc(1.5rem + 1px);
|
|
460
|
+
min-width: calc(1.5rem + 1px);
|
|
461
|
+
height: calc(1.5rem + 1px);
|
|
462
|
+
min-height: calc(1.5rem + 1px);
|
|
463
|
+
color: var(--fg-color, aliceblue);
|
|
464
|
+
background-color: var(--bg-color, steelblue);
|
|
465
|
+
}
|
|
466
|
+
.wrapper {
|
|
467
|
+
width: calc(1.5rem + 1px);
|
|
468
|
+
min-width: calc(1.5rem + 1px);
|
|
469
|
+
height: calc(1.5rem + 1px);
|
|
470
|
+
min-height: calc(1.5rem + 1px);
|
|
471
|
+
color: var(--fg-color, aliceblue);
|
|
472
|
+
background-color: var(--bg-color, steelblue);
|
|
473
|
+
flex-direction: column;
|
|
474
|
+
justify-content: space-around;
|
|
475
|
+
align-items: center;
|
|
476
|
+
padding: 0.2rem;
|
|
477
|
+
display: flex;
|
|
478
|
+
position: fixed;
|
|
479
|
+
top: 0;;
|
|
480
|
+
right: 0;
|
|
481
|
+
z-index: 100;
|
|
482
|
+
cursor: pointer;
|
|
483
|
+
@media screen and (orientation: landscape) {
|
|
484
|
+
display: none;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
.wrapper span {
|
|
488
|
+
position: absolute;
|
|
489
|
+
width: 80%;
|
|
490
|
+
height: 0.25rem;
|
|
491
|
+
background-color: var(--fg-color, aliceblue);
|
|
492
|
+
border-radius: 0.125rem;
|
|
493
|
+
transition: all 0.3s ease;
|
|
494
|
+
}
|
|
495
|
+
.wrapper span:nth-child(1) {
|
|
496
|
+
top: 25%;
|
|
497
|
+
}
|
|
498
|
+
.wrapper span:nth-child(2) {
|
|
499
|
+
top: 50%;
|
|
500
|
+
}
|
|
501
|
+
.wrapper span:nth-child(3) {
|
|
502
|
+
top: 75%;
|
|
503
|
+
}
|
|
504
|
+
:host(.open) .wrapper span:nth-child(1) {
|
|
505
|
+
top: 50%;
|
|
506
|
+
transform: rotate(45deg);
|
|
507
|
+
}
|
|
508
|
+
:host(.open) .wrapper span:nth-child(2) {
|
|
509
|
+
opacity: 0;
|
|
510
|
+
}
|
|
511
|
+
:host(.open) .wrapper span:nth-child(3) {
|
|
512
|
+
top: 50%;
|
|
513
|
+
transform: rotate(-45deg);
|
|
514
|
+
}
|
|
515
|
+
</style>
|
|
516
|
+
`;
|
|
517
|
+
|
|
518
|
+
function hamburgerHtml() {
|
|
519
|
+
return `
|
|
520
|
+
<div class="wrapper">
|
|
521
|
+
<span></span>
|
|
522
|
+
<span></span>
|
|
523
|
+
<span></span>
|
|
524
|
+
</div>
|
|
525
|
+
`;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export class HamburgerButton extends ColorMixin() {
|
|
529
|
+
constructor() {
|
|
530
|
+
super();
|
|
531
|
+
this.boundClick = this.onClick.bind(this);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
connectedCallback() {
|
|
535
|
+
this.shadowRoot.innerHTML = `${hamburgerStyle}${hamburgerHtml()}`;
|
|
536
|
+
this.shadowRoot.querySelector('.wrapper').addEventListener('click', this.boundClick);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
disconnectedCallback() {
|
|
540
|
+
this.shadowRoot.querySelector('.wrapper').removeEventListener('click', this.boundClick);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
onClick(evt) {
|
|
544
|
+
this.toggle();
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
toggle() {
|
|
548
|
+
this.classList.toggle('open');
|
|
549
|
+
this.dispatchToggleEvent();
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
open() {
|
|
553
|
+
this.classList.toggle('open', true);
|
|
554
|
+
this.dispatchToggleEvent();
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
close() {
|
|
558
|
+
this.classList.toggle('open', false);
|
|
559
|
+
this.dispatchToggleEvent();
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
dispatchToggleEvent() {
|
|
563
|
+
const options = {detail: {isOpen: this.isopen}, composed: true, bubbles: true};
|
|
564
|
+
const toggleEvent = new CustomEvent('hamburger-toggle', options);
|
|
565
|
+
this.dispatchEvent(toggleEvent);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
get isopen() {
|
|
569
|
+
return this.classList.contains('open');
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
window.customElements.define('hamburger-button', HamburgerButton);
|
|
574
|
+
|
|
575
|
+
//
|
|
576
|
+
|
|
577
|
+
const flexMenuStyle = `
|
|
578
|
+
<style>
|
|
579
|
+
:host {
|
|
580
|
+
position: fixed;
|
|
581
|
+
top: 0;
|
|
582
|
+
left: 0;
|
|
583
|
+
z-index: 50;
|
|
584
|
+
width: 50vw;
|
|
585
|
+
min-width: 50vw;
|
|
586
|
+
height: calc(1.5rem + 1px);
|
|
587
|
+
min-height: calc(1.5rem + 1px);
|
|
588
|
+
color: var(--fg-color, aliceblue);
|
|
589
|
+
background-color: transparent;
|
|
590
|
+
@media screen and (orientation: landscape) {
|
|
591
|
+
width: 100%;
|
|
592
|
+
min-width: 100%;
|
|
593
|
+
background-color: var(--bg-color, steelblue);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.wrapper {
|
|
598
|
+
position: relative;
|
|
599
|
+
top: 0;
|
|
600
|
+
left: 0;
|
|
601
|
+
display: flex;
|
|
602
|
+
flex-direction: column;
|
|
603
|
+
justify-content: flex-start;
|
|
604
|
+
align-items: left;
|
|
605
|
+
width: 50vw;
|
|
606
|
+
min-width: 10rem;
|
|
607
|
+
height: 100%;
|
|
608
|
+
/* color: var(--fg-color, aliceblue);
|
|
609
|
+
background-color: transparent; */
|
|
610
|
+
@media screen and (orientation: landscape) {
|
|
611
|
+
flex-direction: row;
|
|
612
|
+
align-items: center;
|
|
613
|
+
width: 100%;
|
|
614
|
+
min-width: 100%;
|
|
615
|
+
height: 1.5rem;
|
|
616
|
+
/* background-color: var(--bg-color, steelblue); */
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
.wrapper hamburger-button {
|
|
620
|
+
position: fixed;
|
|
621
|
+
top: 0;
|
|
622
|
+
right: 0;
|
|
623
|
+
z-index: 100;
|
|
624
|
+
}
|
|
625
|
+
.wrapper main-menu {
|
|
626
|
+
position: fixed;
|
|
627
|
+
top: 0;
|
|
628
|
+
left: 0;
|
|
629
|
+
display: none;
|
|
630
|
+
@media screen and (orientation: landscape) {
|
|
631
|
+
display: inherit;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
.wrapper:has(hamburger-button.open) main-menu {
|
|
635
|
+
display: inherit;
|
|
636
|
+
}
|
|
637
|
+
</style>
|
|
638
|
+
`;
|
|
639
|
+
|
|
640
|
+
function flexMenuHtml() {
|
|
641
|
+
return `
|
|
642
|
+
<div class="wrapper">
|
|
643
|
+
<main-menu>
|
|
644
|
+
<slot></slot>
|
|
645
|
+
</main-menu>
|
|
646
|
+
<hamburger-button></hamburger-button>
|
|
647
|
+
</div>
|
|
648
|
+
`;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
export class FlexMenu extends ColorMixin() {
|
|
652
|
+
constructor() {
|
|
653
|
+
super();
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
connectedCallback() {
|
|
657
|
+
this.shadowRoot.innerHTML = `${flexMenuStyle}${flexMenuHtml()}`;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
window.customElements.define('flex-menu', FlexMenu);
|
|
663
|
+
|
|
664
|
+
//
|
package/index.html
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Flexible Menu Test</title>
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
|
7
|
+
<!--[if lt IE 9]><script src="js/html5shiv-printshiv.js" media="all"></script><![endif]-->
|
|
8
|
+
<link rel="stylesheet" href="styles.css" />
|
|
9
|
+
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
|
10
|
+
<script src="flexmenu.js"></script>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<!-- <hamburger-button></hamburger-button> -->
|
|
14
|
+
<flex-menu bg-color="teal">
|
|
15
|
+
<!-- <nav> -->
|
|
16
|
+
<menu-list menu-title="Sites">
|
|
17
|
+
<menu-item use-default-handler href="#home">Home</menu-item>
|
|
18
|
+
<div>River Plate, el más grande, lejos!</div>
|
|
19
|
+
<menu-item use-default-handler href="#contact">Contact</menu-item>
|
|
20
|
+
<span></span>
|
|
21
|
+
<menu-item use-default-handler href="#about">About</menu-item>
|
|
22
|
+
</menu-list>
|
|
23
|
+
<menu-list menu-title="Teams">
|
|
24
|
+
<menu-item use-default-handler href="#river">River Plate</menu-item>
|
|
25
|
+
<menu-item use-default-handler href="#boca">Boca Juniors</menu-item>
|
|
26
|
+
<menu-item use-default-handler href="#racing">Racing Club</menu-item>
|
|
27
|
+
<span></span>
|
|
28
|
+
<menu-list menu-title="2nd Division">
|
|
29
|
+
<menu-item use-default-handler href="#centralCordoba">Central Cordoba</menu-item>
|
|
30
|
+
<menu-item use-default-handler href="#argentino">Argentino</menu-item>
|
|
31
|
+
<menu-item use-default-handler href="#leonesRosario">Leones Rosario</menu-item>
|
|
32
|
+
</menu-list>
|
|
33
|
+
</menu-list>
|
|
34
|
+
<!-- </nav> -->
|
|
35
|
+
</flex-menu>
|
|
36
|
+
<div class="main">
|
|
37
|
+
<!-- <div class="stack"> -->
|
|
38
|
+
<!-- </div> -->
|
|
39
|
+
<hr>
|
|
40
|
+
<h3 class="title-header">
|
|
41
|
+
Flexible Menu Test Page
|
|
42
|
+
</h3>
|
|
43
|
+
<hr>
|
|
44
|
+
<!-- <p>
|
|
45
|
+
<button>Toggle Menu Item</button>
|
|
46
|
+
</p> -->
|
|
47
|
+
<div class="row centered">
|
|
48
|
+
<label for="loc-hash">Location: </label>
|
|
49
|
+
<span class="purple big cornered" id="loc-hash"></span>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
<script src="index.js"></script>
|
|
53
|
+
</body>
|
|
54
|
+
</html>
|
package/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
|
|
2
|
+
let flexmenu, hambutton;
|
|
3
|
+
|
|
4
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
5
|
+
console.log("Document loaded.");
|
|
6
|
+
flexmenu = document.querySelector('flex-menu');
|
|
7
|
+
hambutton = flexmenu.shadowRoot.querySelector('hamburger-button');
|
|
8
|
+
menuitem = document.querySelectorAll('menu-item')[0];
|
|
9
|
+
hambutton.addEventListener('hamburger-toggle', () => console.info(`Hamburger toggled: ${hambutton.isopen ? "open" : "closed"}`));
|
|
10
|
+
document.querySelectorAll('menu-item').forEach(mi => mi.addEventListener('click', ev => {
|
|
11
|
+
console.info(`Click on '${ev.currentTarget.textContent}'`);
|
|
12
|
+
}));
|
|
13
|
+
const hashChange = () => {
|
|
14
|
+
const content = location.hash.length > 1 ?
|
|
15
|
+
`${location.hash[1].toUpperCase()}${location.hash.slice(2)}` : "#";
|
|
16
|
+
document.querySelector('#loc-hash').textContent = content;
|
|
17
|
+
// document.querySelector('menu-item').unselect();
|
|
18
|
+
// if (content === "#") document.querySelectorAll('menu-item').forEach(mi => mi.unselect());
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
window.addEventListener('hashchange', hashChange);
|
|
22
|
+
hashChange();
|
|
23
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@esavoretti/flexmenu",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Menu web component with built-in flexibility to accommodate itself to mobile screens",
|
|
5
|
+
"main": "flexmenu.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [],
|
|
11
|
+
"author": "Domingo E. Savoretti <esavoretti@gmail.com> (https://sandy98.github.io/flexmenu)",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/sandy98/flexmenu.git"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT"
|
|
17
|
+
}
|
package/runserver.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import sys, http.server
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
HOST, PORT = '', 8086
|
|
8
|
+
print(f"Serving web content at ({HOST}, {PORT})")
|
|
9
|
+
server = http.server.ThreadingHTTPServer((HOST, PORT), http.server.SimpleHTTPRequestHandler)
|
|
10
|
+
try:
|
|
11
|
+
server.serve_forever()
|
|
12
|
+
except KeyboardInterrupt:
|
|
13
|
+
print("\nKeyboard interrupt.")
|
|
14
|
+
except Exception as exc:
|
|
15
|
+
print(f"\nUnknown exception: {exc}")
|
|
16
|
+
finally:
|
|
17
|
+
server.server_close()
|
|
18
|
+
print("\nBye")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if __name__ == '__main__':
|
|
22
|
+
main()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
package/styles.css
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
font-size: 16px;
|
|
3
|
+
--fg-color: aliceblue;
|
|
4
|
+
--bg-color: steelblue;
|
|
5
|
+
--selected-color: lightgreen;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
body {
|
|
9
|
+
font-family: Arial, Helvetica, sans-serif;
|
|
10
|
+
margin: 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
header {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
gap: 0;
|
|
17
|
+
justify-content: flex-start;
|
|
18
|
+
align-items: left;
|
|
19
|
+
background: steelblue;
|
|
20
|
+
width: fit-content;
|
|
21
|
+
@media screen and (orientation: landscape) {
|
|
22
|
+
/* header { */
|
|
23
|
+
flex-direction: row;
|
|
24
|
+
align-items: center;
|
|
25
|
+
/* } */
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
nav {
|
|
31
|
+
display: flex;
|
|
32
|
+
flex-direction: row;
|
|
33
|
+
justify-content: flex-start;
|
|
34
|
+
align-items: top;
|
|
35
|
+
flex-wrap: nowrap;
|
|
36
|
+
background-color: transparent;
|
|
37
|
+
gap: 1px;
|
|
38
|
+
color: aliceblue;
|
|
39
|
+
height: calc(10px + 1.6rem);
|
|
40
|
+
padding-left: 0.5rem;
|
|
41
|
+
padding-right: 0.5rem;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
nav ul {
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-direction: row;
|
|
47
|
+
gap: 1rem;
|
|
48
|
+
}
|
|
49
|
+
nav li {
|
|
50
|
+
text-decoration: none;
|
|
51
|
+
list-style-type: none;
|
|
52
|
+
cursor: pointer;
|
|
53
|
+
width: fit-content;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
p {
|
|
57
|
+
margin-left: 0.5rem;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.lightgreen {
|
|
61
|
+
background-color: rgb(190, 252, 190);
|
|
62
|
+
color: black;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.main {
|
|
66
|
+
display: flex;
|
|
67
|
+
flex-direction: column;
|
|
68
|
+
justify-content: stretch;
|
|
69
|
+
align-items: center;
|
|
70
|
+
gap: 0;
|
|
71
|
+
margin: 1rem 3rem auto;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.title-header {
|
|
75
|
+
text-align: center;
|
|
76
|
+
font-weight: bold;
|
|
77
|
+
user-select: none;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.selected {
|
|
81
|
+
background-color: maroon;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.stack {
|
|
85
|
+
display: flex;
|
|
86
|
+
flex-direction: column;
|
|
87
|
+
justify-content: stretch;
|
|
88
|
+
align-items: flex-start;
|
|
89
|
+
gap: 0;
|
|
90
|
+
margin: 1rem 1rem auto;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.row {
|
|
94
|
+
display: inline-flex;
|
|
95
|
+
flex-direction: row;
|
|
96
|
+
justify-content: space-around;
|
|
97
|
+
align-items: center;
|
|
98
|
+
width: 50%;
|
|
99
|
+
padding: 0.3rem;
|
|
100
|
+
border: solid 1px;
|
|
101
|
+
border-radius: 5px;
|
|
102
|
+
margin: 1rem 1rem auto;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.blue {
|
|
106
|
+
background-color: steelblue;
|
|
107
|
+
color: white;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.purple {
|
|
111
|
+
background-color: purple;
|
|
112
|
+
color: white;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.big {
|
|
116
|
+
font-size: 120%;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.cornered {
|
|
120
|
+
border-radius: 5px;
|
|
121
|
+
padding: 10px;
|
|
122
|
+
}
|