@operato/menu 2.0.0-alpha.55

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 (56) hide show
  1. package/.editorconfig +29 -0
  2. package/.storybook/main.js +3 -0
  3. package/.storybook/server.mjs +8 -0
  4. package/CHANGELOG.md +12 -0
  5. package/README.md +75 -0
  6. package/demo/index.html +40 -0
  7. package/dist/src/index.d.ts +0 -0
  8. package/dist/src/index.js +2 -0
  9. package/dist/src/index.js.map +1 -0
  10. package/dist/src/menu-landscape-styles.d.ts +1 -0
  11. package/dist/src/menu-landscape-styles.js +149 -0
  12. package/dist/src/menu-landscape-styles.js.map +1 -0
  13. package/dist/src/menu-portrait-styles.d.ts +1 -0
  14. package/dist/src/menu-portrait-styles.js +147 -0
  15. package/dist/src/menu-portrait-styles.js.map +1 -0
  16. package/dist/src/ox-menu-landscape.d.ts +21 -0
  17. package/dist/src/ox-menu-landscape.js +99 -0
  18. package/dist/src/ox-menu-landscape.js.map +1 -0
  19. package/dist/src/ox-menu-part.d.ts +29 -0
  20. package/dist/src/ox-menu-part.js +135 -0
  21. package/dist/src/ox-menu-part.js.map +1 -0
  22. package/dist/src/ox-menu-portrait.d.ts +13 -0
  23. package/dist/src/ox-menu-portrait.js +85 -0
  24. package/dist/src/ox-menu-portrait.js.map +1 -0
  25. package/dist/src/ox-top-menu-bar.d.ts +23 -0
  26. package/dist/src/ox-top-menu-bar.js +145 -0
  27. package/dist/src/ox-top-menu-bar.js.map +1 -0
  28. package/dist/src/types.d.ts +10 -0
  29. package/dist/src/types.js +2 -0
  30. package/dist/src/types.js.map +1 -0
  31. package/dist/stories/ox-menu-container.d.ts +15 -0
  32. package/dist/stories/ox-menu-container.js +94 -0
  33. package/dist/stories/ox-menu-container.js.map +1 -0
  34. package/dist/stories/ox-menu-portrait.stories.d.ts +17 -0
  35. package/dist/stories/ox-menu-portrait.stories.js +35 -0
  36. package/dist/stories/ox-menu-portrait.stories.js.map +1 -0
  37. package/dist/stories/test-menus.d.ts +2 -0
  38. package/dist/stories/test-menus.js +179 -0
  39. package/dist/stories/test-menus.js.map +1 -0
  40. package/dist/tsconfig.tsbuildinfo +1 -0
  41. package/package.json +100 -0
  42. package/src/index.ts +0 -0
  43. package/src/menu-landscape-styles.ts +149 -0
  44. package/src/menu-portrait-styles.ts +147 -0
  45. package/src/ox-menu-landscape.ts +105 -0
  46. package/src/ox-menu-part.ts +131 -0
  47. package/src/ox-menu-portrait.ts +87 -0
  48. package/src/ox-top-menu-bar.ts +147 -0
  49. package/src/types.ts +10 -0
  50. package/stories/ox-menu-container.ts +97 -0
  51. package/stories/ox-menu-portrait.stories.ts +46 -0
  52. package/stories/test-menus.ts +180 -0
  53. package/themes/app-theme.css +145 -0
  54. package/tsconfig.json +23 -0
  55. package/web-dev-server.config.mjs +27 -0
  56. package/web-test-runner.config.mjs +41 -0
@@ -0,0 +1,147 @@
1
+ import { css } from 'lit'
2
+
3
+ export const MenuPortraitStyles = css`
4
+ :host {
5
+ display: flex;
6
+ overflow-y: auto;
7
+ flex-direction: column;
8
+ height: 100%;
9
+ }
10
+
11
+ :host > ul {
12
+ margin-block-end: 1.5em;
13
+ }
14
+
15
+ ul {
16
+ list-style: none;
17
+ margin: 0;
18
+ padding: 0;
19
+ }
20
+
21
+ [group-label] {
22
+ padding: 25px 0 var(--padding-narrow) var(--padding-wide);
23
+ border-bottom: var(--border-dark-color);
24
+ font: bold 12px var(--theme-font);
25
+ color: rgba(var(--primary-color-rgb), 0.9);
26
+ text-transform: uppercase;
27
+ }
28
+
29
+ a {
30
+ display: flex;
31
+ align-items: center;
32
+ border-bottom: 1px solid rgba(0, 0, 0, 0.07);
33
+ padding: var(--padding-default) var(--padding-default) var(--padding-default) var(--padding-wide);
34
+ text-decoration: none;
35
+ font: normal 14px var(--theme-font);
36
+ color: var(--secondary-color);
37
+ text-transform: capitalize;
38
+
39
+ overflow: hidden;
40
+ white-space: nowrap;
41
+ text-overflow: ellipsis;
42
+ }
43
+
44
+ a:hover {
45
+ color: var(--primary-color);
46
+ font-weight: bold;
47
+ }
48
+
49
+ a * {
50
+ vertical-align: middle;
51
+ }
52
+
53
+ a mwc-icon {
54
+ margin-right: var(--margin-narrow);
55
+ font-size: 15px;
56
+ color: rgba(var(--secondary-color-rgb), 0.7);
57
+ }
58
+
59
+ a [submenu-button] {
60
+ float: left;
61
+ font-size: 15px;
62
+ max-height: 20px;
63
+ }
64
+
65
+ a [submenu-button]::before {
66
+ content: 'add_box';
67
+ }
68
+
69
+ li[active] > a [submenu-button]::before {
70
+ content: 'indeterminate_check_box';
71
+ }
72
+
73
+ li[active] > a {
74
+ border-left: 3px solid var(--primary-color);
75
+ font-weight: bold;
76
+ color: var(--primary-color);
77
+ }
78
+
79
+ li li a {
80
+ padding: 7px 0 7px 35px;
81
+ font: normal 13px var(--theme-font);
82
+ color: var(--secondary-color);
83
+ }
84
+
85
+ li li[active] a {
86
+ background-color: rgba(var(--primary-color-rgb), 0.15);
87
+ font: bold 13px var(--theme-font);
88
+ color: var(--primary-color);
89
+ }
90
+
91
+ li > ul {
92
+ overflow-y: hidden;
93
+ max-height: 0;
94
+ background-color: #f6f6f6;
95
+
96
+ transition-property: all;
97
+ transition-duration: 0.7s;
98
+ }
99
+
100
+ li[active] > ul {
101
+ max-height: 500px;
102
+ }
103
+
104
+ li[active] > ul[settled] {
105
+ overflow-y: auto;
106
+ }
107
+
108
+ li li a::before {
109
+ margin-right: var(--margin-narrow);
110
+ }
111
+
112
+ a [badge] {
113
+ margin-left: auto;
114
+ background-color: var(--primary-background-color);
115
+ color: white;
116
+ border-radius: 999em;
117
+ padding: 0px 6px;
118
+ }
119
+
120
+ @media only screen and (max-width: 460px) {
121
+ :host {
122
+ min-width: 100vw;
123
+ }
124
+
125
+ a {
126
+ padding: var(--padding-wide);
127
+ font: normal 15px var(--theme-font);
128
+ }
129
+
130
+ li[active] ul {
131
+ border-bottom: 2px solid rgba(0, 0, 0, 0.1);
132
+ }
133
+
134
+ li li a {
135
+ display: block;
136
+ padding: var(--padding-wide) var(--padding-default) var(--padding-wide) 35px;
137
+ overflow: hidden;
138
+ text-overflow: ellipsis;
139
+ white-space: nowrap;
140
+ font: normal 14px var(--theme-font);
141
+ }
142
+
143
+ li li[active] a {
144
+ font: bold 14px var(--theme-font);
145
+ }
146
+ }
147
+ `
@@ -0,0 +1,105 @@
1
+ import '@material/mwc-icon'
2
+
3
+ import { html, LitElement } from 'lit'
4
+ import { customElement, property, query, state } from 'lit/decorators.js'
5
+ import { connect } from 'pwa-helpers'
6
+
7
+ import { navigate, store } from '@operato/shell'
8
+ import { ScrollbarStyles } from '@operato/styles'
9
+
10
+ import { Menu } from './types'
11
+ import { MenuLandscapeStyles } from './menu-landscape-styles'
12
+
13
+ @customElement('ox-menu-landscape')
14
+ export class OxMenuLandscape extends connect(store)(LitElement) {
15
+ static styles = [ScrollbarStyles, MenuLandscapeStyles]
16
+
17
+ @property({ type: Array }) menus?: Menu[]
18
+ @property({ type: Object }) activeTopLevel?: Menu
19
+ @property({ type: Object }) activeMenu!: Menu
20
+ @property({ type: String }) path?: string
21
+
22
+ render() {
23
+ const { menus = [], activeTopLevel, activeMenu } = this
24
+
25
+ return html`
26
+ <div id="wrap" @mousewheel=${this.onWheelEvent.bind(this)}>
27
+ <ul>
28
+ ${menus.map(menu =>
29
+ menu.type == 'group'
30
+ ? html``
31
+ : html`
32
+ <li ?active=${menu === activeTopLevel}>
33
+ <a href=${menu.path || '#'}>
34
+ ${menu.icon ? html`<mwc-icon>${menu.icon}</mwc-icon>` : html``} ${menu.name}
35
+ </a>
36
+
37
+ <ul submenus>
38
+ ${menu.menus?.map(
39
+ menu => html`
40
+ <li ?active=${menu === activeMenu}>
41
+ <a href=${menu.path || '#'}> ${menu.name} </a>
42
+ </li>
43
+ `
44
+ )}
45
+ </ul>
46
+
47
+ <div description>
48
+ ${menu.icon ? html`<mwc-icon>${menu.icon}</mwc-icon>` : html``} ${menu.description || ''}
49
+ </div>
50
+ </li>
51
+ `
52
+ )}
53
+ </ul>
54
+ </div>
55
+ `
56
+ }
57
+
58
+ firstUpdated() {
59
+ this.renderRoot.addEventListener('click', (e: Event) => {
60
+ //@ts-ignore
61
+ if (e.target.submenu) {
62
+ /* protect to act move to href. */
63
+ e.stopPropagation()
64
+ e.preventDefault()
65
+
66
+ //@ts-ignore
67
+ let menu = e.target.submenu
68
+
69
+ this.dispatchEvent(
70
+ new CustomEvent('active-toplevel', {
71
+ bubbles: true,
72
+ detail: this.activeTopLevel === menu ? undefined : menu
73
+ })
74
+ )
75
+
76
+ return
77
+ }
78
+
79
+ /* to respond even if current acting menu is selected */
80
+ let href = (e.target as HTMLAnchorElement).href
81
+ href && location.href === href && navigate(href + '#force', true)
82
+ })
83
+
84
+ /* to hide scrollbar during transition */
85
+ this.renderRoot.addEventListener('transitionstart', e => {
86
+ ;(e.target as HTMLElement).removeAttribute('settled')
87
+ })
88
+ this.renderRoot.addEventListener('transitionend', e => {
89
+ ;(e.target as HTMLElement).setAttribute('settled', '')
90
+ })
91
+ }
92
+
93
+ onWheelEvent(e: WheelEvent) {
94
+ const { target, deltaY, detail } = e
95
+
96
+ if (!(target instanceof HTMLElement)) {
97
+ return
98
+ }
99
+
100
+ const delta = deltaY || -detail
101
+ target.scrollLeft -= (delta / Math.abs(delta)) * 10
102
+
103
+ e.preventDefault()
104
+ }
105
+ }
@@ -0,0 +1,131 @@
1
+ import '@material/mwc-icon'
2
+ import './ox-menu-portrait'
3
+ import './ox-menu-landscape'
4
+
5
+ import { css, html, LitElement, PropertyValues } from 'lit'
6
+ import { customElement, property, query, state } from 'lit/decorators.js'
7
+ import { connect } from 'pwa-helpers'
8
+
9
+ import { store } from '@operato/shell'
10
+ import { ScrollbarStyles } from '@operato/styles'
11
+
12
+ import { Menu } from './types'
13
+
14
+ function isActiveMenu(menu: Menu, path: string) {
15
+ return (
16
+ menu.path?.split('?')[0] === path ||
17
+ (menu.active && typeof menu.active === 'function' && menu.active.call(menu, { path }))
18
+ )
19
+ }
20
+
21
+ @customElement('ox-menu-part')
22
+ export class OxMenuPart extends connect(store)(LitElement) {
23
+ static styles = [
24
+ ScrollbarStyles,
25
+ css`
26
+ :host {
27
+ display: flex;
28
+ overflow-y: auto;
29
+ flex-direction: column;
30
+ height: 100%;
31
+ min-width: 200px;
32
+ background-color: var(--theme-white-color);
33
+ }
34
+
35
+ :host([landscape]) {
36
+ overflow-x: auto;
37
+ flex-direction: row;
38
+ width: 100%;
39
+ min-height: 20px;
40
+ }
41
+
42
+ ox-menu-portrait,
43
+ ox-menu-landscape {
44
+ flex: 1;
45
+ }
46
+ `
47
+ ]
48
+
49
+ @property({ type: String }) page!: string
50
+ @property({ type: String }) resourceId?: string
51
+ @property({ type: Array }) menus?: Menu[]
52
+ @property({ type: String }) orientation?: 'landscape' | 'portrait'
53
+
54
+ @state() slotTemplate: any
55
+ @state() _activeTopLevel?: Menu
56
+ @state() _activeMenu?: Menu
57
+ @state() _path?: string
58
+
59
+ render() {
60
+ return html`
61
+ <slot name="head"></slot>
62
+ ${this.orientation !== 'landscape'
63
+ ? html`<ox-menu-portrait
64
+ .menus=${this.menus}
65
+ .activeTopLevel=${this._activeTopLevel}
66
+ .activeMenu=${this._activeMenu}
67
+ .path=${this._path}
68
+ ></ox-menu-portrait>`
69
+ : html`<ox-menu-landscape
70
+ .menus=${this.menus}
71
+ .activeTopLevel=${this._activeTopLevel}
72
+ .activeMenu=${this._activeMenu}
73
+ .path=${this._path}
74
+ ></ox-menu-landscape>`}
75
+ <slot name="tail"></slot>
76
+ `
77
+ }
78
+
79
+ firstUpdated() {
80
+ this.renderRoot.addEventListener('active-toplevel', (e: Event) => {
81
+ e.stopPropagation()
82
+ e.preventDefault()
83
+
84
+ this._activeTopLevel = (e as CustomEvent).detail
85
+ })
86
+ }
87
+
88
+ updated(changes: PropertyValues<this>) {
89
+ if (changes.has('menus') || changes.has('page') || changes.has('resourceId')) {
90
+ this.findActivePage()
91
+ }
92
+
93
+ if (changes.has('orientation')) {
94
+ if (this.orientation == 'portrait') {
95
+ this.removeAttribute('landscape')
96
+ } else {
97
+ this.setAttribute('landscape', '')
98
+ }
99
+ }
100
+
101
+ if (changes.has('slotTemplate')) {
102
+ this.replaceChild(this.slotTemplate, this.renderRoot)
103
+ }
104
+ }
105
+
106
+ stateChanged(state: any): void {
107
+ this.page = state.route.page
108
+ this.resourceId = state.route.resourceId
109
+ this.menus = state.liteMenu.menus || []
110
+ this.slotTemplate = state.liteMenu.slotTemplate
111
+ }
112
+
113
+ private findActivePage() {
114
+ var path = this.resourceId ? `${this.page}/${this.resourceId}` : this.page
115
+ var menus = this.menus || []
116
+ var activeMenu
117
+
118
+ this._activeTopLevel = menus.find(menu => {
119
+ if (isActiveMenu(menu, path)) {
120
+ activeMenu = menu
121
+ return true
122
+ } else if (menu.menus) {
123
+ activeMenu = menu.menus.find(menu => isActiveMenu(menu, path))
124
+ return !!activeMenu
125
+ }
126
+ })
127
+
128
+ this._path = path
129
+ this._activeMenu = activeMenu || this._activeTopLevel
130
+ }
131
+ }
@@ -0,0 +1,87 @@
1
+ import '@material/mwc-icon'
2
+
3
+ import { html, LitElement, TemplateResult } from 'lit'
4
+ import { customElement, property } from 'lit/decorators.js'
5
+
6
+ import { navigate } from '@operato/shell'
7
+ import { ScrollbarStyles } from '@operato/styles'
8
+
9
+ import { Menu } from './types'
10
+ import { MenuPortraitStyles } from './menu-portrait-styles'
11
+
12
+ @customElement('ox-menu-portrait')
13
+ export class OxMenuPortrait extends LitElement {
14
+ static styles = [ScrollbarStyles, MenuPortraitStyles]
15
+
16
+ @property({ type: Array }) menus?: Menu[]
17
+ @property({ type: Object }) activeTopLevel?: Menu
18
+ @property({ type: Object }) activeMenu?: Menu
19
+ @property({ type: String }) path!: string
20
+
21
+ renderMenus(menus: Menu[], activeTopLevel?: Menu, activeMenu?: Menu): TemplateResult {
22
+ return html`
23
+ <ul>
24
+ ${menus.map(menu => {
25
+ var { type, active, path, name, badge, icon, menus = [] } = menu
26
+ active = active && typeof active === 'function' ? active.call(menu, { path: this.path }) : false
27
+ badge = typeof badge === 'function' ? badge.call(menu) : badge ?? false
28
+
29
+ return type == 'group'
30
+ ? html`<li group-label>${name}</li>`
31
+ : html`
32
+ <li ?active=${activeTopLevel ? menu === activeTopLevel : active} .menu=${menu} menu>
33
+ <a href=${path || '#'}>
34
+ ${menus.length > 0 ? html` <mwc-icon submenu-button></mwc-icon> ` : html``}
35
+ <mwc-icon>${icon}</mwc-icon>
36
+ ${name} ${badge !== false ? html`<div badge>${badge}</div>` : html``}
37
+ </a>
38
+ ${menus && this.renderMenus(menus || [], activeMenu)}
39
+ </li>
40
+ `
41
+ })}
42
+ </ul>
43
+ `
44
+ }
45
+
46
+ render() {
47
+ const { menus, activeTopLevel, activeMenu } = this
48
+ return this.renderMenus(menus || [], activeTopLevel, activeMenu)
49
+ }
50
+
51
+ firstUpdated() {
52
+ this.renderRoot.addEventListener('click', (e: Event) => {
53
+ const menuElement = (e.target as Element)!.closest('[menu]')
54
+
55
+ //@ts-ignore
56
+ if (menuElement?.menu) {
57
+ //@ts-ignore
58
+ let menu = menuElement.menu
59
+
60
+ if (!menu.path) {
61
+ /* protect to act move to href. */
62
+ e.stopPropagation()
63
+ e.preventDefault()
64
+ }
65
+
66
+ this.dispatchEvent(
67
+ new CustomEvent('active-toplevel', {
68
+ bubbles: true,
69
+ detail: this.activeTopLevel === menu ? undefined : menu
70
+ })
71
+ )
72
+ }
73
+
74
+ /* to respond even if current acting menu is selected */
75
+ let href = (e.target as HTMLAnchorElement)!.href
76
+ href && location.href === href && navigate(href + '#force', true)
77
+ })
78
+
79
+ /* to hide scrollbar during transition */
80
+ this.renderRoot.addEventListener('transitionstart', e => {
81
+ ;(e.target as Element).removeAttribute('settled')
82
+ })
83
+ this.renderRoot.addEventListener('transitionend', e => {
84
+ ;(e.target as Element).setAttribute('settled', '')
85
+ })
86
+ }
87
+ }
@@ -0,0 +1,147 @@
1
+ import '@material/mwc-icon'
2
+
3
+ import { css, html, LitElement, PropertyValueMap, TemplateResult } from 'lit'
4
+ import { customElement, property, query, state } from 'lit/decorators.js'
5
+ import { connect } from 'pwa-helpers'
6
+
7
+ import { toggleOverlay } from '@operato/layout'
8
+ import { store } from '@operato/shell'
9
+
10
+ import { Menu } from './types'
11
+
12
+ @customElement('ox-top-menu-bar')
13
+ export class OxTopMenuBar extends connect(store)(LitElement) {
14
+ static styles = [
15
+ css`
16
+ :host {
17
+ display: flex;
18
+ flex-direction: row;
19
+ }
20
+
21
+ span {
22
+ flex: 1;
23
+ }
24
+
25
+ ul {
26
+ display: flex;
27
+ align-items: center;
28
+ list-style: none;
29
+ margin: 0;
30
+ padding: 0;
31
+ }
32
+
33
+ li {
34
+ display: inline-flex;
35
+ flex-direction: row nowrap;
36
+ float: left;
37
+ overflow: none;
38
+ }
39
+
40
+ a {
41
+ display: inline-block;
42
+ white-space: nowrap;
43
+ overflow: hidden;
44
+ text-overflow: ellipsis;
45
+ padding: var(--padding-default) var(--padding-wide) var(--padding-narrow) var(--padding-wide);
46
+ text-decoration: none;
47
+ color: white;
48
+ }
49
+ a * {
50
+ vertical-align: middle;
51
+ }
52
+ a mwc-icon {
53
+ opacity: 0.5;
54
+ position: relative;
55
+ top: -2px;
56
+ font-size: 1em;
57
+ }
58
+
59
+ li[active] a {
60
+ font-weight: bold;
61
+ }
62
+ li[active] a mwc-icon {
63
+ opacity: 1;
64
+ }
65
+ `
66
+ ]
67
+
68
+ @property({ type: String }) page?: string
69
+ @property({ type: String }) resourceId?: string
70
+ @property({ type: Array }) menus?: Menu[]
71
+ @property({ type: Object }) slotTemplate!: Node
72
+
73
+ @state() private _activeTopLevel?: Menu
74
+
75
+ render() {
76
+ const { menus = [], _activeTopLevel } = this
77
+
78
+ return html`
79
+ <slot name="head"></slot>
80
+ <ul>
81
+ ${menus.map(menu =>
82
+ menu.type == 'group'
83
+ ? html``
84
+ : html`
85
+ <li ?active=${menu === _activeTopLevel}>
86
+ <a
87
+ href="#"
88
+ @click=${(e: MouseEvent) => {
89
+ e.preventDefault()
90
+ toggleOverlay('ox-menu-part', {
91
+ backdrop: true
92
+ })
93
+ }}
94
+ >
95
+ ${menu.name}
96
+ <mwc-icon>expand_more</mwc-icon>
97
+ </a>
98
+ </li>
99
+ `
100
+ )}
101
+ </ul>
102
+ <slot name="tail"></slot>
103
+ `
104
+ }
105
+
106
+ stateChanged(state: any): void {
107
+ this.page = state.route.page
108
+ this.resourceId = state.route.resourceId
109
+ this.menus = state.liteMenu.menus || []
110
+ this.slotTemplate = state.liteMenu.slotTemplate
111
+ }
112
+
113
+ // firstUpdated() {
114
+ // this.addEventListener('mousewheel', this.onWheelEvent.bind(this), false)
115
+ // }
116
+
117
+ updated(changes: PropertyValueMap<this>) {
118
+ if (changes.has('menus') || changes.has('page') || changes.has('resourceId')) {
119
+ this._findActivePage()
120
+ }
121
+
122
+ if (changes.has('slotTemplate')) {
123
+ this.replaceChild(this.slotTemplate, this.renderRoot)
124
+ }
125
+ }
126
+
127
+ // onWheelEvent(e) {
128
+ // var delta = Math.max(-1, Math.min(1, e.wheelDelta || -e.detail))
129
+ // this.scrollLeft -= delta * 40
130
+ // console.log('delta', this.scrollLeft, delta, e.wheelDelta || -e.detail)
131
+
132
+ // e.preventDefault()
133
+ // }
134
+
135
+ _findActivePage() {
136
+ var path = this.resourceId ? `${this.page}/${this.resourceId}` : this.page
137
+ var menus = this.menus || []
138
+
139
+ this._activeTopLevel = menus.find(menu => {
140
+ if (menu.path?.split('?')[0] === path) {
141
+ return true
142
+ } else if (menu.menus) {
143
+ return !!menu.menus.find(menu => menu.path?.split('?')[0] === path)
144
+ }
145
+ })
146
+ }
147
+ }
package/src/types.ts ADDED
@@ -0,0 +1,10 @@
1
+ export type Menu = {
2
+ type?: 'group' | 'board'
3
+ name?: string
4
+ description?: string
5
+ path?: string
6
+ icon?: string
7
+ badge?: boolean | string | (() => boolean)
8
+ active?: boolean | ((menu?: Menu) => boolean)
9
+ menus?: Menu[]
10
+ }