5htp-core 0.2.6 → 0.2.7

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.
Files changed (32) hide show
  1. package/package.json +5 -4
  2. package/src/client/app/index.ts +2 -2
  3. package/src/client/assets/css/text/icons.less +8 -2
  4. package/src/client/assets/css/utils/layouts.less +3 -0
  5. package/src/client/components/Form.ts +2 -2
  6. package/src/client/components/index.ts +2 -0
  7. package/src/client/components/input/Checkbox/index.less +0 -0
  8. package/src/client/components/input/Checkbox/index.tsx +58 -50
  9. package/src/client/components/input/Checkbox/old.tsx +74 -0
  10. package/src/client/components/inputv3/base.tsx +13 -1
  11. package/src/client/components/inputv3/date/index.tsx +49 -0
  12. package/src/client/components/inputv3/date/react-calendar.less +143 -0
  13. package/src/client/components/inputv3/date/react-daterange-picker.less +112 -0
  14. package/src/client/pages/useHeader.tsx +3 -2
  15. package/src/client/services/router/components/router.tsx +3 -2
  16. package/src/client/services/router/request/api.ts +0 -5
  17. package/src/client/services/router/request/index.ts +10 -0
  18. package/src/client/services/router/request/multipart.ts +120 -9
  19. package/src/server/app/config.ts +2 -0
  20. package/src/server/app/index.ts +4 -2
  21. package/src/server/services/router/http/index.ts +7 -3
  22. package/src/server/services/router/index.ts +8 -1
  23. package/src/server/services/router/response/index.ts +3 -1
  24. package/src/server/services/router/response/page/document.tsx +7 -19
  25. package/src/server/services/router/response/page/index.tsx +17 -1
  26. package/src/server/services/router/service.ts +1 -1
  27. package/src/types/global/modules.d.ts +1 -1
  28. package/src/client/components/input/Date/index.less +0 -167
  29. package/src/client/components/input/Date/index.tsx +0 -90
  30. package/src/client/services/metrics/index.ts +0 -37
  31. package/src/server/services/metrics/detect.ts +0 -109
  32. package/src/server/services/metrics/index.ts +0 -272
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "5htp-core",
3
3
  "description": "Convenient TypeScript framework designed for Performance and Productivity.",
4
- "version": "0.2.6",
4
+ "version": "0.2.7",
5
5
  "author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
6
6
  "repository": "git://github.com/gaetanlegac/5htp-core.git",
7
7
  "license": "MIT",
@@ -13,6 +13,7 @@
13
13
  "framework"
14
14
  ],
15
15
  "dependencies": {
16
+ "@wojtekmaj/react-daterange-picker": "^4.2.0",
16
17
  "accepts": "^1.3.7",
17
18
  "activity-detector": "^3.0.0",
18
19
  "ansi-to-html": "^0.7.1",
@@ -93,7 +94,7 @@
93
94
  "@types/yargs-parser": "^21.0.0",
94
95
  "babel-plugin-glob-import": "^0.0.6-2"
95
96
  },
96
- "peerDependencies": {
97
- "5htp": "0.2.3"
98
- }
97
+ "peerDependencies": {
98
+ "5htp": "0.2.3"
99
+ }
99
100
  }
@@ -24,8 +24,8 @@ export { default as Service } from './service';
24
24
  declare global {
25
25
  interface Window {
26
26
  dev: boolean,
27
- // Defined by loading gtag.js
28
- gtag: (action: string, name: string, params?: any) => void,
27
+ /*context: ClientContext,
28
+ user: User,*/
29
29
  /*context: ClientContext,
30
30
  user: User,*/
31
31
  }
@@ -64,6 +64,7 @@ img.icon {
64
64
  object-position: center;
65
65
  }
66
66
 
67
+ img.logo,
67
68
  i.logo {
68
69
  /*width: 3.2em;
69
70
  height: 3.2em;
@@ -75,11 +76,16 @@ i.logo {
75
76
  line-height: 2em;
76
77
  flex: 0 0 2em;
77
78
 
79
+ background-color: var(--cBg2);
80
+ border-radius: @radius;
81
+ border: none; // For img
82
+ }
83
+
84
+ i.logo {
85
+
78
86
  background-repeat: no-repeat;
79
87
  background-position: center;
80
88
  background-size: cover;
81
- background-color: var(--cBg2);
82
- border-radius: 50%;
83
89
 
84
90
  &.contain {
85
91
  background-color: transparent;
@@ -118,6 +118,7 @@
118
118
  &.al-bottom { align-items: flex-end; }
119
119
  &.al-fill { align-items: stretch; }
120
120
 
121
+ > .row-1 { align-self: stretch; }
121
122
  }
122
123
 
123
124
  .col {
@@ -134,6 +135,8 @@
134
135
  &, &.al-fill { align-items: stretch; }
135
136
 
136
137
  &.sep-1 > * + * { border-top: solid 1px var(--cLine); }
138
+
139
+ > .col-1 { align-self: stretch; }
137
140
  }
138
141
 
139
142
  .row,
@@ -86,7 +86,7 @@ export default function useForm<TFormData extends {}>(
86
86
 
87
87
  // Autosave
88
88
  if (options.autoSave !== undefined)
89
- saveLocally(data, options.autoSave);
89
+ saveLocally(data, options.autoSave.id);
90
90
 
91
91
  }, [data]);
92
92
 
@@ -134,7 +134,7 @@ export default function useForm<TFormData extends {}>(
134
134
 
135
135
  // Reset autosaved data
136
136
  if (options.autoSave)
137
- localStorage.removeItem(options.autoSave.id);
137
+ localStorage.removeItem('form.' + options.autoSave.id);
138
138
 
139
139
  return submitResult;
140
140
  }
@@ -16,6 +16,7 @@ export { default as Video } from './Video';
16
16
  export { default as Number } from './input/Number';
17
17
  export { default as Slider } from './input/Slider';
18
18
  export { default as Radio } from './input/Radio';
19
+ export { default as Checkbox } from './input/Checkbox';
19
20
 
20
21
  // Data
21
22
  export { default as Amount } from './Amount';
@@ -28,6 +29,7 @@ export { default as CircularProgressbar } from './data/progressbar/circular';
28
29
  export { default as Select } from './Select';
29
30
  export { default as Input } from './inputv3';
30
31
  export { default as File } from './inputv3/file';
32
+ export { default as DateRangeInput } from './inputv3/date';
31
33
 
32
34
  // TOD: fix popover component
33
35
  //export { default as Date } from './input/Date';
File without changes
@@ -1,74 +1,82 @@
1
1
  /*----------------------------------
2
2
  - DEPENDANCES
3
3
  ----------------------------------*/
4
+
5
+ // Npm
4
6
  import React from 'react';
5
- import Champ from '../Base';
6
- import { ComponentChild } from 'preact';
7
+ import { JSX, ComponentChild } from 'preact';
7
8
 
8
- import uid from '@common/data/id';
9
+ // Core libs
10
+ import { useInput, InputBaseProps } from '../../inputv3/base';
9
11
 
10
12
  /*----------------------------------
11
13
  - TYPES
12
14
  ----------------------------------*/
13
- type TValeur = boolean;
14
- const valeurDefaut = false as boolean;
15
- type TValeurDefaut = typeof valeurDefaut;
16
- type TValeurOut = string;
17
-
18
15
  export type Props = {
19
- valeur: TValeur,
20
- switch?: boolean,
21
- children?: ComponentChild
16
+ id: string,
17
+ label: ComponentChild,
18
+ // State
19
+ inputRef?: React.Ref<HTMLInputElement>
22
20
  }
23
21
 
24
22
  /*----------------------------------
25
- - COMPOSANTS
23
+ - COMPOSANT
26
24
  ----------------------------------*/
27
- import './index.less';
28
- export default Champ<Props, TValeurDefaut, TValeurOut>('checkbox', { valeurDefaut }, ({
29
- label, titre, children, switch: isSwitch, attrsContChamp, attrsChamp, nom, description,
30
- readOnly
31
- }, { valeur, state, setState }, rendre) => {
25
+ export default ({
26
+ id,
27
+ // Decoration
28
+ required,
29
+ label: labelText,
30
+ // State
31
+ inputRef, errors,
32
+ ...props
33
+ }: Props & InputBaseProps<boolean> & Omit<JSX.HTMLAttributes<HTMLInputElement>, 'onChange'>) => {
32
34
 
33
- if (label === undefined)
34
- label = true;
35
- if (!titre && children)
36
- titre = children;
35
+ /*----------------------------------
36
+ - INIT
37
+ ----------------------------------*/
37
38
 
38
- if (isSwitch) {
39
- attrsContChamp.className += ' switch';
40
- if (label)
41
- attrsContChamp.className += ' avecLabel';
42
- } else
43
- attrsContChamp.className += ' classique';
39
+ const [{ value, focus, fieldProps }, setValue, commitValue, setState] = useInput(props, false, true);
44
40
 
45
- if (state.chargement)
46
- attrsContChamp.disabled = true;
41
+ const refInput = inputRef || React.useRef<HTMLInputElement>();
42
+
43
+ /*----------------------------------
44
+ - ATTRIBUTES
45
+ ----------------------------------*/
47
46
 
48
- attrsChamp.id = 'check_' + (nom || uid());
47
+ let className: string = 'input checkbox row';
49
48
 
50
- if (readOnly)
51
- return rendre(valeur ? 'Yes' : 'No', {});
49
+ if (focus)
50
+ className += ' focus';
51
+ if (errors?.length)
52
+ className += ' error';
52
53
 
53
- return rendre(<>
54
+ if (props.className !== undefined)
55
+ className += ' ' + props.className;
54
56
 
55
- <input type="checkbox" {...attrsChamp}
56
- onChange={() => {
57
- setState({ valeur: !valeur });
58
- }}
59
- checked={valeur}
60
- />
57
+ /*----------------------------------
58
+ - RENDER
59
+ ----------------------------------*/
60
+ return <>
61
+ <div class={className} onClick={() => refInput.current?.focus()}>
61
62
 
62
- {/* On ne peut pas rendre le switch + le texte du label dans le même <label> */}
63
- {(true || (isSwitch && label)) && (
64
- <label htmlFor={attrsChamp.id} />
65
- )}
63
+ <input type="checkbox"
64
+ onChange={() => {
65
+ setValue( !value );
66
+ }}
67
+ checked={value}
68
+ />
66
69
 
67
- <div className="contLabel">
68
- <label htmlFor={attrsChamp.id}>{titre}</label>
69
-
70
- {description && <p className="desc">{description}</p>}
70
+ <label htmlFor={id} class="col-1 txt-left">
71
+ {labelText}
72
+ </label>
73
+
71
74
  </div>
72
-
73
- </>, { ecraser: true });
74
- })
75
+
76
+ {errors?.length && (
77
+ <div class="fg error txt-left">
78
+ {errors.join('. ')}
79
+ </div>
80
+ )}
81
+ </>
82
+ }
@@ -0,0 +1,74 @@
1
+ /*----------------------------------
2
+ - DEPENDANCES
3
+ ----------------------------------*/
4
+ import React from 'react';
5
+ import Champ from '../Base';
6
+ import { ComponentChild } from 'preact';
7
+
8
+ import uid from '@common/data/id';
9
+
10
+ /*----------------------------------
11
+ - TYPES
12
+ ----------------------------------*/
13
+ type TValeur = boolean;
14
+ const valeurDefaut = false as boolean;
15
+ type TValeurDefaut = typeof valeurDefaut;
16
+ type TValeurOut = string;
17
+
18
+ export type Props = {
19
+ valeur: TValeur,
20
+ switch?: boolean,
21
+ children?: ComponentChild
22
+ }
23
+
24
+ /*----------------------------------
25
+ - COMPOSANTS
26
+ ----------------------------------*/
27
+ import './index.less';
28
+ export default Champ<Props, TValeurDefaut, TValeurOut>('checkbox', { valeurDefaut }, ({
29
+ label, titre, children, switch: isSwitch, attrsContChamp, attrsChamp, nom, description,
30
+ readOnly
31
+ }, { valeur, state, setState }, rendre) => {
32
+
33
+ if (label === undefined)
34
+ label = true;
35
+ if (!titre && children)
36
+ titre = children;
37
+
38
+ if (isSwitch) {
39
+ attrsContChamp.className += ' switch';
40
+ if (label)
41
+ attrsContChamp.className += ' avecLabel';
42
+ } else
43
+ attrsContChamp.className += ' classique';
44
+
45
+ if (state.chargement)
46
+ attrsContChamp.disabled = true;
47
+
48
+ attrsChamp.id = 'check_' + (nom || uid());
49
+
50
+ if (readOnly)
51
+ return rendre(valeur ? 'Yes' : 'No', {});
52
+
53
+ return rendre(<>
54
+
55
+ <input type="checkbox" {...attrsChamp}
56
+ onChange={() => {
57
+ setState({ valeur: !valeur });
58
+ }}
59
+ checked={valeur}
60
+ />
61
+
62
+ {/* On ne peut pas rendre le switch + le texte du label dans le même <label> */}
63
+ {(true || (isSwitch && label)) && (
64
+ <label htmlFor={attrsChamp.id} />
65
+ )}
66
+
67
+ <div className="contLabel">
68
+ <label htmlFor={attrsChamp.id}>{titre}</label>
69
+
70
+ {description && <p className="desc">{description}</p>}
71
+ </div>
72
+
73
+ </>, { ecraser: true });
74
+ })
@@ -37,6 +37,7 @@ export type TInputState<TValue> = {
37
37
  export function useInput<TValue>(
38
38
  { value: externalValue, onChange }: InputBaseProps<TValue>,
39
39
  defaultValue: TValue,
40
+ autoCommit: boolean = false
40
41
  ): [
41
42
  state: TInputState<TValue>,
42
43
  setValue: (value: TValue) => void,
@@ -52,7 +53,12 @@ export function useInput<TValue>(
52
53
  changed: false
53
54
  });
54
55
 
55
- const setValue = (value: TValue) => setState({ value, valueSource: 'internal', changed: true });
56
+ const setValue = (value: TValue) => {
57
+ setState({ value, valueSource: 'internal', changed: true });
58
+
59
+ if (autoCommit)
60
+ commitValue(value);
61
+ };
56
62
 
57
63
  const commitValue = () => {
58
64
 
@@ -75,6 +81,12 @@ export function useInput<TValue>(
75
81
 
76
82
  }, [externalValue]);
77
83
 
84
+ React.useEffect(() => {
85
+ if (state.valueSource === 'internal') {
86
+ commitValue();
87
+ }
88
+ }, [state.value]);
89
+
78
90
  return [state, setValue, commitValue, setState]
79
91
  }
80
92
 
@@ -0,0 +1,49 @@
1
+ /*----------------------------------
2
+ - DEPENDANCES
3
+ ----------------------------------*/
4
+
5
+ // Npm
6
+ import React from 'react';
7
+ import type { StateUpdater } from 'preact/hooks';
8
+ import DateRangePicker from '@wojtekmaj/react-daterange-picker/dist/entry.nostyle';
9
+
10
+ // Core
11
+
12
+ // App
13
+
14
+ /*----------------------------------
15
+ - TYPES
16
+ ----------------------------------*/
17
+
18
+ type TValue = [Date, Date]
19
+ export type Props = {
20
+ value: TValue,
21
+ onChange: StateUpdater<TValue>,
22
+ placeholder?: string,
23
+ min?: string,
24
+ max?: string
25
+ }
26
+
27
+ /*----------------------------------
28
+ - COMPOSANT
29
+ ----------------------------------*/
30
+ import './react-calendar.less';
31
+ import './react-daterange-picker.less';
32
+ export default ({ value, Props, min, max, onChange }) => {
33
+
34
+ const state = React.useState(false);
35
+
36
+ /*----------------------------------
37
+ - CONSTRUCTION CHAMP
38
+ ----------------------------------*/
39
+
40
+
41
+ /*----------------------------------
42
+ - RENDU DU CHAMP
43
+ ----------------------------------*/
44
+ return (
45
+ <div>
46
+ <DateRangePicker onChange={onChange} value={value || [null, null]} />
47
+ </div>
48
+ )
49
+ }
@@ -0,0 +1,143 @@
1
+ .react-calendar {
2
+ width: 350px;
3
+ max-width: 100%;
4
+ background: white;
5
+ border: 1px solid #a0a096;
6
+ font-family: Arial, Helvetica, sans-serif;
7
+ line-height: 1.125em;
8
+ }
9
+
10
+ .react-calendar--doubleView {
11
+ width: 700px;
12
+ }
13
+
14
+ .react-calendar--doubleView .react-calendar__viewContainer {
15
+ display: flex;
16
+ margin: -0.5em;
17
+ }
18
+
19
+ .react-calendar--doubleView .react-calendar__viewContainer > * {
20
+ width: 50%;
21
+ margin: 0.5em;
22
+ }
23
+
24
+ .react-calendar,
25
+ .react-calendar *,
26
+ .react-calendar *:before,
27
+ .react-calendar *:after {
28
+ -moz-box-sizing: border-box;
29
+ -webkit-box-sizing: border-box;
30
+ box-sizing: border-box;
31
+ }
32
+
33
+ .react-calendar button {
34
+ margin: 0;
35
+ border: 0;
36
+ outline: none;
37
+ }
38
+
39
+ .react-calendar button:enabled:hover {
40
+ cursor: pointer;
41
+ }
42
+
43
+ .react-calendar__navigation {
44
+ display: flex;
45
+ height: 44px;
46
+ margin-bottom: 1em;
47
+ }
48
+
49
+ .react-calendar__navigation button {
50
+ min-width: 44px;
51
+ background: none;
52
+ }
53
+
54
+ .react-calendar__navigation button:disabled {
55
+ background-color: #f0f0f0;
56
+ }
57
+
58
+ .react-calendar__navigation button:enabled:hover,
59
+ .react-calendar__navigation button:enabled:focus {
60
+ background-color: #e6e6e6;
61
+ }
62
+
63
+ .react-calendar__month-view__weekdays {
64
+ text-align: center;
65
+ text-transform: uppercase;
66
+ font-weight: bold;
67
+ font-size: 0.75em;
68
+ }
69
+
70
+ .react-calendar__month-view__weekdays__weekday {
71
+ padding: 0.5em;
72
+ }
73
+
74
+ .react-calendar__month-view__weekNumbers .react-calendar__tile {
75
+ display: flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ font-size: 0.75em;
79
+ font-weight: bold;
80
+ }
81
+
82
+ .react-calendar__month-view__days__day--weekend {
83
+ color: #d10000;
84
+ }
85
+
86
+ .react-calendar__month-view__days__day--neighboringMonth {
87
+ color: #757575;
88
+ }
89
+
90
+ .react-calendar__year-view .react-calendar__tile,
91
+ .react-calendar__decade-view .react-calendar__tile,
92
+ .react-calendar__century-view .react-calendar__tile {
93
+ padding: 2em 0.5em;
94
+ }
95
+
96
+ .react-calendar__tile {
97
+ max-width: 100%;
98
+ padding: 10px 6.6667px;
99
+ background: none;
100
+ text-align: center;
101
+ line-height: 16px;
102
+ }
103
+
104
+ .react-calendar__tile:disabled {
105
+ background-color: #f0f0f0;
106
+ }
107
+
108
+ .react-calendar__tile:enabled:hover,
109
+ .react-calendar__tile:enabled:focus {
110
+ background-color: #e6e6e6;
111
+ }
112
+
113
+ .react-calendar__tile--now {
114
+ background: #ffff76;
115
+ }
116
+
117
+ .react-calendar__tile--now:enabled:hover,
118
+ .react-calendar__tile--now:enabled:focus {
119
+ background: #ffffa9;
120
+ }
121
+
122
+ .react-calendar__tile--hasActive {
123
+ background: #76baff;
124
+ }
125
+
126
+ .react-calendar__tile--hasActive:enabled:hover,
127
+ .react-calendar__tile--hasActive:enabled:focus {
128
+ background: #a9d4ff;
129
+ }
130
+
131
+ .react-calendar__tile--active {
132
+ background: #006edc;
133
+ color: white;
134
+ }
135
+
136
+ .react-calendar__tile--active:enabled:hover,
137
+ .react-calendar__tile--active:enabled:focus {
138
+ background: #1087ff;
139
+ }
140
+
141
+ .react-calendar--selectRange .react-calendar__tile--hover {
142
+ background-color: #e6e6e6;
143
+ }
@@ -0,0 +1,112 @@
1
+ .react-daterange-picker {
2
+ display: inline-flex;
3
+ position: relative;
4
+ }
5
+
6
+ .react-daterange-picker,
7
+ .react-daterange-picker *,
8
+ .react-daterange-picker *:before,
9
+ .react-daterange-picker *:after {
10
+ -moz-box-sizing: border-box;
11
+ -webkit-box-sizing: border-box;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ .react-daterange-picker--disabled {
16
+ background-color: #f0f0f0;
17
+ color: #6d6d6d;
18
+ }
19
+
20
+ .react-daterange-picker__wrapper {
21
+ display: flex;
22
+ flex-grow: 1;
23
+ flex-shrink: 0;
24
+ align-items: center;
25
+ border: thin solid gray;
26
+ }
27
+
28
+ .react-daterange-picker__inputGroup {
29
+ min-width: calc((4px * 3) + 0.54em * 8 + 0.217em * 2);
30
+ height: 100%;
31
+ flex-grow: 1;
32
+ padding: 0 2px;
33
+ box-sizing: content-box;
34
+ }
35
+
36
+ .react-daterange-picker__inputGroup__divider {
37
+ padding: 1px 0;
38
+ white-space: pre;
39
+ }
40
+
41
+ .react-daterange-picker__inputGroup__divider,
42
+ .react-daterange-picker__inputGroup__leadingZero {
43
+ display: inline-block;
44
+ }
45
+
46
+ .react-daterange-picker__inputGroup__input {
47
+ min-width: 0.54em;
48
+ height: 100%;
49
+ position: relative;
50
+ padding: 0 1px;
51
+ border: 0;
52
+ background: none;
53
+ font: inherit;
54
+ box-sizing: content-box;
55
+ -webkit-appearance: textfield;
56
+ -moz-appearance: textfield;
57
+ appearance: textfield;
58
+ }
59
+
60
+ .react-daterange-picker__inputGroup__input::-webkit-outer-spin-button,
61
+ .react-daterange-picker__inputGroup__input::-webkit-inner-spin-button {
62
+ -webkit-appearance: none;
63
+ -moz-appearance: none;
64
+ appearance: none;
65
+ margin: 0;
66
+ }
67
+
68
+ .react-daterange-picker__inputGroup__input:invalid {
69
+ background: rgba(255, 0, 0, 0.1);
70
+ }
71
+
72
+ .react-daterange-picker__inputGroup__input--hasLeadingZero {
73
+ margin-left: -0.54em;
74
+ padding-left: calc(1px + 0.54em);
75
+ }
76
+
77
+ .react-daterange-picker__button {
78
+ border: 0;
79
+ background: transparent;
80
+ padding: 4px 6px;
81
+ }
82
+
83
+ .react-daterange-picker__button:enabled {
84
+ cursor: pointer;
85
+ }
86
+
87
+ .react-daterange-picker__button:enabled:hover .react-daterange-picker__button__icon,
88
+ .react-daterange-picker__button:enabled:focus .react-daterange-picker__button__icon {
89
+ stroke: #0078d7;
90
+ }
91
+
92
+ .react-daterange-picker__button:disabled .react-daterange-picker__button__icon {
93
+ stroke: #6d6d6d;
94
+ }
95
+
96
+ .react-daterange-picker__button svg {
97
+ display: inherit;
98
+ }
99
+
100
+ .react-daterange-picker__calendar {
101
+ width: 350px;
102
+ max-width: 100vw;
103
+ z-index: 1;
104
+ }
105
+
106
+ .react-daterange-picker__calendar--closed {
107
+ display: none;
108
+ }
109
+
110
+ .react-daterange-picker__calendar .react-calendar {
111
+ border-width: thin;
112
+ }
@@ -29,10 +29,11 @@ export default ({ id, title, subtitle, focus, jail, error }: Props) => {
29
29
  if (!page)
30
30
  return;
31
31
 
32
- const fullTitle = title + ' | ' + subtitle;
32
+ page.title = title;
33
+ if (subtitle !== undefined)
34
+ page.title += ' | ' + subtitle;
33
35
 
34
36
  page.bodyId = page.bodyId || id || '';
35
- page.title = fullTitle;
36
37
 
37
38
  if (focus)
38
39
  page.bodyClass.add('focus');