@peter.naydenov/shortcuts 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/src/listen.js ADDED
@@ -0,0 +1,186 @@
1
+ 'use strict'
2
+
3
+
4
+
5
+ function listen ( dependencies, options, currentContext ) { // Listen for input signals and generate event titles
6
+ const {
7
+ specialChars
8
+ , readKeyEvent
9
+ , readMouseEvent
10
+ , findTarget
11
+ , ev
12
+ , exposeShortcut
13
+ , streamKeys
14
+ } = dependencies
15
+ , {
16
+ mouseWait
17
+ , keyWait
18
+ , clickTarget
19
+ , listenFor
20
+ } = options
21
+ ;
22
+
23
+ let
24
+ r = []
25
+ , mouseTarget = null // Dom element or null
26
+ , mouseDomEvent = null
27
+ , keyTimer = null // Timer for key sequence or null
28
+ , mouseTimer = null // Timer for mouse sequence or null
29
+ , mouseIgnore = null // Timer for ignoring mouse clicks or null
30
+ , sequence = true
31
+ , ignore = false // Use to trigger a single callback without adding the key to the sequence.
32
+ , count = 0
33
+ ;
34
+
35
+
36
+
37
+
38
+ const
39
+ waitKeys = () => sequence = false
40
+ , endKeys = () => sequence = true
41
+ , ignoreKeys = () => ignore = true
42
+ , waitingKeys = () => sequence === false
43
+ ;
44
+
45
+
46
+
47
+ function keySequenceEnd () { // Execute when key sequence ends
48
+ let res = r.map ( x => ([x.join('+')]) );
49
+ if ( !sequence ) {
50
+ let signal = res.at(-1);
51
+ ev.emit ( signal, { wait:waitKeys, end:endKeys, ignore:ignoreKeys, isWaiting:waitingKeys, note: currentContext.note, context: currentContext.name })
52
+ if ( ignore ) {
53
+ res = res.slice ( 0, -1 )
54
+ ignore = false
55
+ }
56
+ }
57
+
58
+ if ( sequence ) {
59
+ ev.emit ( res.join(','), { wait: waitKeys, end:endKeys, ignore:ignoreKeys, isWaiting:waitingKeys, note: currentContext.note, context: currentContext.name })
60
+ if ( exposeShortcut ) exposeShortcut ( res.join(','), currentContext.name, currentContext.note ) // TODO: Add a context information...?
61
+ // Reset:
62
+ r = []
63
+ keyTimer = null
64
+ }
65
+ } // keySequeceEnd func.
66
+
67
+
68
+
69
+ function mouseSequenceEnd () { // Execute when mouse sequence ends
70
+ const
71
+ mouseEvent = readMouseEvent ( mouseDomEvent, count )
72
+ , data = {
73
+ target : mouseTarget
74
+ , targetProps : mouseTarget ? mouseTarget.getBoundingClientRect() : null
75
+ , x : mouseDomEvent.clientX
76
+ , y : mouseDomEvent.clientY
77
+ , context : currentContext.name
78
+ , note : currentContext.note
79
+ , event : mouseDomEvent
80
+ }
81
+ ;
82
+ ev.emit ( mouseEvent.join('+'), data )
83
+ if ( exposeShortcut ) exposeShortcut ( mouseEvent.join('+'), currentContext.name, currentContext.note )
84
+ // Reset:
85
+ mouseTimer = null
86
+ mouseIgnore = null
87
+ mouseTarget = null
88
+ mouseDomEvent = null
89
+ count = 0
90
+ } // mouseSequenceEnd func.
91
+
92
+
93
+
94
+ function listenMouse () {
95
+ window.addEventListener ( 'contextmenu', event => { // Listen for right mouse clicks
96
+ clearTimeout ( mouseTimer )
97
+ if ( mouseIgnore ) {
98
+ clearTimeout ( mouseIgnore )
99
+ mouseIgnore = setTimeout ( () => mouseIgnore=null, mouseWait )
100
+ return
101
+ }
102
+ if ( count === options.maxClicks ) {
103
+ mouseSequenceEnd ()
104
+ mouseIgnore = setTimeout ( () => mouseIgnore=null, mouseWait )
105
+ return
106
+ }
107
+ event.preventDefault ()
108
+ mouseTarget = findTarget (event.target, clickTarget )
109
+ mouseDomEvent = event
110
+ count++
111
+ mouseTimer = setTimeout ( mouseSequenceEnd, mouseWait )
112
+ })
113
+
114
+ document.addEventListener ( 'click', event => { // Listen for left and middle mouse clicks
115
+ clearTimeout ( mouseTimer )
116
+ if ( mouseIgnore ) {
117
+ clearTimeout ( mouseIgnore )
118
+ mouseIgnore = setTimeout ( () => mouseIgnore=null, mouseWait )
119
+ return
120
+ }
121
+ if ( count === options.maxClicks ) {
122
+ mouseSequenceEnd ()
123
+ mouseIgnore = setTimeout ( () => mouseIgnore=null, mouseWait )
124
+ return
125
+ }
126
+ mouseTarget = findTarget ( event.target, clickTarget )
127
+ mouseDomEvent = event
128
+ count++
129
+ mouseTimer = setTimeout ( mouseSequenceEnd, mouseWait )
130
+ })
131
+ } // listenMouse func.
132
+
133
+
134
+
135
+ function listenKeyboard () {
136
+ document.addEventListener ( 'keydown', event => { // Listen for special keyboard keys
137
+ clearTimeout ( keyTimer )
138
+ if ( specialChars.hasOwnProperty(event.code) ) r.push ( readKeyEvent ( event, specialChars ))
139
+ else return
140
+ if ( streamKeys ) streamKeys ( event.key, currentContext.name, currentContext.note )
141
+ if ( options.keyIgnore ) {
142
+ clearTimeout ( options.keyIgnore )
143
+ options.keyIgnore = setTimeout ( () => options.keyIgnore=null, keyWait )
144
+ return
145
+ }
146
+ if ( sequence && r.length === options.maxSequence ) {
147
+ keySequenceEnd ()
148
+ options.keyIgnore = setTimeout ( () => options.keyIgnore=null, keyWait )
149
+ return
150
+ }
151
+ if ( sequence ) keyTimer = setTimeout ( keySequenceEnd, keyWait )
152
+ else keySequenceEnd ()
153
+ })
154
+
155
+ document.addEventListener ( 'keypress', event => { // Listen for regular keyboard keys
156
+ if ( specialChars.hasOwnProperty(event.code) ) return
157
+ clearTimeout ( keyTimer )
158
+ if ( streamKeys ) streamKeys ( event.key, currentContext.name, currentContext.note )
159
+ if ( options.keyIgnore ) {
160
+ clearTimeout ( options.keyIgnore )
161
+ options.keyIgnore = setTimeout ( () => options.keyIgnore=null, keyWait )
162
+ return
163
+ }
164
+ r.push ( readKeyEvent ( event, specialChars ))
165
+ if ( sequence && r.length === options.maxSequence ) {
166
+ keySequenceEnd ()
167
+ options.keyIgnore = setTimeout ( () => options.keyIgnore=null, keyWait )
168
+ return
169
+ }
170
+ if ( sequence ) keyTimer = setTimeout ( keySequenceEnd, keyWait )
171
+ else keySequenceEnd ()
172
+ })
173
+ } // listenKeyboard func.
174
+
175
+
176
+
177
+ if ( listenFor.includes('mouse') ) listenMouse ()
178
+ if ( listenFor.includes('keyboard') ) listenKeyboard ()
179
+
180
+ } // listen func.
181
+
182
+
183
+
184
+ export default listen
185
+
186
+
package/src/load.js ADDED
@@ -0,0 +1,32 @@
1
+ 'use strict'
2
+
3
+
4
+
5
+ function load ( shortcuts, readShortcut, changeContext, getContext ) {
6
+ return function load ( obj ) {
7
+ const
8
+ currentContextName = getContext ()
9
+ , contextList = Object.keys ( obj )
10
+ ;
11
+ let hasChanges = false;
12
+ contextList.forEach ( contextName => {
13
+ const description = Object.entries ( obj [ contextName ] );
14
+ if ( contextName === currentContextName ) hasChanges = true;
15
+ shortcuts [ contextName ] = {}
16
+ description.forEach ( ([ title, callbackList ]) => {
17
+ const name = readShortcut ( title );
18
+ if ( callbackList instanceof Function ) callbackList = [ callbackList ]
19
+ shortcuts [contextName][ name ] = callbackList
20
+ }) // shortcuts.forEach
21
+ }) // contextList.forEach
22
+ if ( hasChanges ) { // Reload context shortcuts after loading process if context was active
23
+ changeContext ()
24
+ changeContext ( currentContextName )
25
+ }
26
+ }} // load func.
27
+
28
+
29
+
30
+ export default load
31
+
32
+
package/src/main.js ADDED
@@ -0,0 +1,92 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Shortcuts
5
+ * ========
6
+ * Create shortcuts for your web application based on keyboard and mouse events.
7
+ * Repository: https://github.com/PeterNaydenov/shortcuts
8
+ *
9
+ * History notes:
10
+ * - Development was started on June 21st, 2023
11
+ * - First version was published on August 14th, 2023
12
+ */
13
+
14
+
15
+
16
+ import notice from '@peter.naydenov/notice' // Docs: https://github.com/PeterNaydenov/notice
17
+
18
+ import listen from './listen.js'
19
+ import readShortcut from './readShortcut.js'
20
+ import readKeyEvent from './readKeyEvent.js'
21
+ import readMouseEvent from './readMouseEvent.js'
22
+ import findTarget from './findTarget.js'
23
+ import specialChars from './specialChars.js'
24
+ import load from './load.js'
25
+ import unload from './unload.js'
26
+ import changeContext from './changeContext.js'
27
+
28
+
29
+
30
+
31
+
32
+ function main ( options = {} ) {
33
+ const
34
+ ev = notice () // Event emitter instance
35
+ , currentContext = { name: null, note: null } // Context data container
36
+ , exposeShortcut = (options.onShortcut && ( typeof options.onShortcut === 'function')) ? options.onShortcut : false
37
+ , streamKeys = (options.streamKeys && ( typeof options.streamKeys === 'function')) ? options.streamKeys : false
38
+ , listenOptions = {
39
+ mouseWait : options.mouseWait ? options.mouseWait : 320 // 320 ms
40
+ , maxClicks : 1 // The maximum number of clicks in a sequence. Controlled automatically by 'changeContext' function.
41
+ , keyWait : options.keyWait ? options.keyWait : 480 // 480 ms
42
+ , maxSequence : 1 // How many keys can be pressed in a sequence. Controlled automatically by 'changeContext' function.
43
+ , clickTarget : options.clickTarget ? options.clickTarget : 'click' // Data-attribute name for click target ( data-click )
44
+ , listenFor : (options.listenFor && Array.isArray(options.listenFor)) ? options.listenFor : [ 'mouse', 'keyboard' ] // What to listen for: ['mouse'], ['keyboard'], ['mouse', 'keyboard']
45
+ , keyIgnore : null // Timer for ignoring key presses after max sequence or null. Not a public option.
46
+ }
47
+ , shortcuts = {} // shortcuts = { contextName : { shortcut : callback[] } }
48
+ , getContext = () => currentContext.name
49
+ , getNote = () => currentContext.note
50
+ , setNote = (note=null) => { if (typeof note === 'string' || note == null ) currentContext.note = note }
51
+ , dependencies = {
52
+ specialChars
53
+ , readKeyEvent
54
+ , readMouseEvent
55
+ , findTarget
56
+ , ev
57
+ , exposeShortcut
58
+ , streamKeys
59
+ }
60
+ ;
61
+
62
+ listen ( dependencies, listenOptions, currentContext )
63
+
64
+ return { // shortcuts API
65
+ load : load ( shortcuts, readShortcut, changeContext( shortcuts, listenOptions, ev, currentContext ), getContext )
66
+ , unload : unload ( shortcuts, ev, currentContext )
67
+ , changeContext : changeContext ( shortcuts, listenOptions, ev, currentContext )
68
+ , pause : () => ev.stop ()
69
+ , resume : () => ev.start ()
70
+ , listContexts : () => Object.keys ( shortcuts )
71
+ , getContext
72
+ , getNote
73
+ , setNote
74
+ }
75
+ } // main func.
76
+
77
+
78
+
79
+ main.getDefaults = () => ({
80
+ mouseWait : 320 // 320 ms // TODO: Slow down. It's too fast at the moment.
81
+ , keyWait : 480 // 480 ms
82
+ , clickTarget : 'click' // Data-attribute name for click target ( data-click )
83
+ , listenFor : [ 'mouse', 'keyboard' ]
84
+ , onShortcut : false // Shortcut log function or false
85
+ , streamKeys : false // Stream keys function or false
86
+ })
87
+
88
+
89
+
90
+ export default main
91
+
92
+
@@ -0,0 +1,27 @@
1
+ 'use strict'
2
+
3
+
4
+ function readKeyEvent ( event, specialChars ) {
5
+ let
6
+ { shiftKey, altKey, ctrlKey } = event
7
+ , falseKeys = [ 'ControlLeft','ControlRight', 'ShiftLeft', 'ShiftRight', 'AltLeft', 'AltRight', 'Meta' ]
8
+ , key = event.code
9
+ .replace ( 'Key', '' )
10
+ .replace('Digit','')
11
+ , res = []
12
+ ;
13
+
14
+ if ( ctrlKey ) res.push ( 'CTRL' )
15
+ if ( shiftKey ) res.push ( 'SHIFT' )
16
+ if ( altKey ) res.push ( 'ALT' )
17
+
18
+ if ( specialChars.hasOwnProperty ( key ) ) res.push ( specialChars[key].toUpperCase () )
19
+ else if ( !falseKeys.includes(key) ) res.push ( key.toUpperCase () )
20
+ return res.sort ()
21
+ } // readKeyEvent func.
22
+
23
+
24
+
25
+ export default readKeyEvent
26
+
27
+
@@ -0,0 +1,24 @@
1
+ 'use strict'
2
+
3
+
4
+ function readMouseEvent ( event, count ) {
5
+ let
6
+ { shiftKey, altKey, ctrlKey, key, button } = event
7
+ , mouseNames = [ 'LEFT', 'MIDDLE', 'RIGHT' ]
8
+ , mouseEvent = `MOUSE-CLICK-${mouseNames[button]}-${count}`
9
+ , res = []
10
+ ;
11
+
12
+ res.push ( mouseEvent )
13
+ if ( ctrlKey ) res.push ( 'CTRL' )
14
+ if ( shiftKey ) res.push ( 'SHIFT' )
15
+ if ( altKey ) res.push ( 'ALT' )
16
+
17
+ return res.sort ()
18
+ } // readMouseEvent func.
19
+
20
+
21
+
22
+ export default readMouseEvent
23
+
24
+
@@ -0,0 +1,16 @@
1
+ 'use strict'
2
+ // [ crtl+s, shift+alt+o]
3
+ function readShortcut ( txt ) {
4
+ const r = txt
5
+ .split ( ',' )
6
+ .map ( (x) => x.trim() )
7
+ .map ( (x) => x.split ( '+' ).map(x => x.toUpperCase()).sort().join('+') )
8
+ .join ( ',' );
9
+ return r
10
+ } // readShortcut func.
11
+
12
+
13
+
14
+ export default readShortcut
15
+
16
+
@@ -0,0 +1,31 @@
1
+ export default {
2
+ 'ArrowLeft' : 'LEFT'
3
+ , 'ArrowUp' : 'UP'
4
+ , 'ArrowRight' : 'RIGHT'
5
+ , 'ArrowDown' : 'DOWN'
6
+ , 'Enter' : 'ENTER'
7
+ , 'NumpadEnter' : 'ENTER'
8
+ , 'Escape' : 'ESC'
9
+ , 'Backspace' : 'BACKSPACE'
10
+ , 'Space' : 'SPACE'
11
+ , 'Tab' : 'TAB'
12
+ , 'Backquote' : '`'
13
+ , 'BracketLeft' : '['
14
+ , 'BracketRight': ']'
15
+ , 'Equal' : '='
16
+ , 'Slash' : '/'
17
+ , 'Backslash' : '\\'
18
+ , 'IntlBackslash' : '`'
19
+ , 'F1' : 'F1'
20
+ , 'F2' : 'F2'
21
+ , 'F3' : 'F3'
22
+ , 'F4' : 'F4'
23
+ , 'F5' : 'F5'
24
+ , 'F6' : 'F6'
25
+ , 'F7' : 'F7'
26
+ , 'F8' : 'F8'
27
+ , 'F9' : 'F9'
28
+ , 'F10' : 'F10'
29
+ , 'F11' : 'F11'
30
+ , 'F12' : 'F12'
31
+ }
package/src/unload.js ADDED
@@ -0,0 +1,23 @@
1
+ 'use strict'
2
+
3
+
4
+
5
+ function unload ( shortcuts, ev, currentContext ) {
6
+ return function unload ( contextName ) {
7
+ const current = currentContext.name;
8
+ if ( current === contextName ) {
9
+ ev.emit ( 'shortcuts-error', `Context '${ contextName }' can't be removed during is current active context. Change the context first` )
10
+ return
11
+ }
12
+ if ( !shortcuts [ contextName ] ) {
13
+ ev.emit ( 'shortcuts-error', `Context '${ contextName }' does not exist` )
14
+ return
15
+ }
16
+ delete shortcuts [ contextName ]
17
+ }} // unload func.
18
+
19
+
20
+
21
+ export default unload
22
+
23
+
package/style.css ADDED
@@ -0,0 +1,97 @@
1
+ :root {
2
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3
+ line-height: 1.5;
4
+ font-weight: 400;
5
+
6
+ color-scheme: light dark;
7
+ color: rgba(255, 255, 255, 0.87);
8
+ background-color: #242424;
9
+
10
+ font-synthesis: none;
11
+ text-rendering: optimizeLegibility;
12
+ -webkit-font-smoothing: antialiased;
13
+ -moz-osx-font-smoothing: grayscale;
14
+ -webkit-text-size-adjust: 100%;
15
+ }
16
+
17
+ a {
18
+ font-weight: 500;
19
+ color: #646cff;
20
+ text-decoration: inherit;
21
+ }
22
+ a:hover {
23
+ color: #535bf2;
24
+ }
25
+
26
+ body {
27
+ margin: 0;
28
+ display: flex;
29
+ place-items: center;
30
+ min-width: 320px;
31
+ min-height: 100vh;
32
+ }
33
+
34
+ h1 {
35
+ font-size: 3.2em;
36
+ line-height: 1.1;
37
+ }
38
+
39
+ #app {
40
+ max-width: 1280px;
41
+ margin: 0 auto;
42
+ padding: 2rem;
43
+ text-align: center;
44
+ }
45
+
46
+ .logo {
47
+ height: 6em;
48
+ padding: 1.5em;
49
+ will-change: filter;
50
+ transition: filter 300ms;
51
+ }
52
+ .logo:hover {
53
+ filter: drop-shadow(0 0 2em #646cffaa);
54
+ }
55
+ .logo.vanilla:hover {
56
+ filter: drop-shadow(0 0 2em #f7df1eaa);
57
+ }
58
+
59
+ .card {
60
+ padding: 2em;
61
+ }
62
+
63
+ .read-the-docs {
64
+ color: #888;
65
+ }
66
+
67
+ button {
68
+ border-radius: 8px;
69
+ border: 1px solid transparent;
70
+ padding: 0.6em 1.2em;
71
+ font-size: 1em;
72
+ font-weight: 500;
73
+ font-family: inherit;
74
+ background-color: #1a1a1a;
75
+ cursor: pointer;
76
+ transition: border-color 0.25s;
77
+ }
78
+ button:hover {
79
+ border-color: #646cff;
80
+ }
81
+ button:focus,
82
+ button:focus-visible {
83
+ outline: 4px auto -webkit-focus-ring-color;
84
+ }
85
+
86
+ @media (prefers-color-scheme: light) {
87
+ :root {
88
+ color: #213547;
89
+ background-color: #ffffff;
90
+ }
91
+ a:hover {
92
+ color: #747bff;
93
+ }
94
+ button {
95
+ background-color: #f9f9f9;
96
+ }
97
+ }
@@ -0,0 +1,111 @@
1
+
2
+ import Block from '../test-components/Block.jsx'
3
+ import '../test-components/style.css'
4
+ import shortcuts from '../src/main.js'
5
+ import { expect } from 'chai'
6
+
7
+ let
8
+ a = false
9
+ , b = false
10
+ ;
11
+ const short = shortcuts ({onShortcut : ( shortcut, context, note ) => console.log (shortcut, context, note)});
12
+ short.load ({
13
+ general : {
14
+ 'shift+a': [ () => a = true ]
15
+ }
16
+ , extra : {
17
+ 'shift+a,p,r,o,b,a,ctrl+m' : () => b = true
18
+ }
19
+ })
20
+
21
+
22
+ describe ( 'Shortcuts', () => {
23
+
24
+ beforeEach ( () => {
25
+ cy.mount ( Block () )
26
+ a = false, b = false
27
+ }) // beforeEach
28
+
29
+ it ( 'Simple shortcut', done => {
30
+ let res = false;
31
+ short.changeContext ( 'general' )
32
+ cy.get('body').type ( '{shift}a' )
33
+ cy.wait ( 100 ) // Default wait sequence timeout is 480 ms
34
+ .then ( () => {
35
+ expect ( a ).to.be.true
36
+ done ()
37
+ })
38
+ }) // it first test
39
+
40
+
41
+
42
+ it ( 'Check context switching and shortcut sequences', done => {
43
+ expect ( a ).to.be.false
44
+ expect ( b ).to.be.false
45
+
46
+ short.changeContext ( 'extra' )
47
+ cy.get('body').type ( '{shift}a' )
48
+ cy.wait ( 500 ) // Default wait sequence timeout is 480 ms. Context 'extra' has a sequence of 7 keys. Need to wait for timeout
49
+ .then ( () => {
50
+ expect ( a ).to.be.false
51
+
52
+ cy.get('body')
53
+ .type ( '{shift}a' )
54
+ .type('proba')
55
+ .type('{ctrl}M')
56
+ return cy.wait ( 1 ) // Shortuct sequence is 7 keys - the maximum number of keys for this context. Don't need to wait for timeout
57
+ })
58
+ .then ( () => {
59
+ expect ( b ).to.be.true
60
+
61
+ short.changeContext ( 'general' )
62
+ cy.get('body').type ( '{shift}a' )
63
+ return cy.wait ( 2 ) // Context 'general' has a sequence of 1 key. Don't need to wait for timeout
64
+ })
65
+ .then ( () => {
66
+ expect ( a ).to.be.true
67
+ done ()
68
+ })
69
+ }) // it context and shortcut sequences
70
+
71
+
72
+
73
+ it ( 'Single mouse click', done => {
74
+ expect ( a ).to.be.false
75
+ expect ( b ).to.be.false
76
+ short.load ({ 'extra' : {
77
+ 'mouse-click-left-1' : () => a = true
78
+ }
79
+ })
80
+ short.changeContext ( 'extra' )
81
+ cy.get('#rspan').click ()
82
+ cy.wait ( 350 ) // Default wait mouse timeout is 320 ms
83
+ .then ( () => {
84
+ expect ( a ).to.be.true
85
+ done ()
86
+ })
87
+ }) // it mouse click
88
+
89
+
90
+
91
+ it ( 'Double mouse click', done => {
92
+ expect ( a ).to.be.false
93
+ expect ( b ).to.be.false
94
+
95
+ short.changeContext ( 'extra' )
96
+
97
+ short.load ({
98
+ 'extra' : { // load will overwrite existing 'extra' context definition
99
+ 'mouse-click-left-2' : () => a = true
100
+ }
101
+ }) // load will restart the selected context
102
+
103
+ cy.get('#rspan').click().click ().click () // Third click is ignored. Max clicks according definition is 2.
104
+ cy.wait ( 350 ) // Default wait mouse timeout is 320 ms
105
+ .then ( () => {
106
+ expect ( a ).to.be.true
107
+ done ()
108
+ })
109
+ }) // it double mouse click
110
+
111
+ }) // describe
@@ -0,0 +1,11 @@
1
+ function Block () {
2
+ return <>
3
+ <div className="block" data-click="red"><span id='rspan'>Red</span> </div>
4
+ <button className="big-btn" data-click="mega">Mega button</button>
5
+ </>
6
+ }
7
+
8
+
9
+ export default Block
10
+
11
+
@@ -0,0 +1,21 @@
1
+ .block {
2
+ width: 100px;
3
+ height: 100px;
4
+ padding: 20px;
5
+ background-color: red;
6
+ }
7
+
8
+ .block span {
9
+ color: white;
10
+ font-size: 20px;
11
+ font-weight: bold;
12
+ font-family: Arial, Helvetica, sans-serif;
13
+ }
14
+
15
+ .big-btn {
16
+ width: 200px;
17
+ height: 200px;
18
+ margin-left: 230px;
19
+ background-color: skyblue;
20
+ border-radius: 10px;
21
+ }