aleman 2.0.2 → 2.0.4

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 (104) hide show
  1. package/ChangeLog +27 -0
  2. package/example/lunes/index.html +3 -0
  3. package/example/lunes/index.js +22 -0
  4. package/example/menu/index.html +1 -0
  5. package/example/menu/index.js +13 -2
  6. package/lunes/actions/index.js +7 -0
  7. package/lunes/actions/up.json +5 -0
  8. package/lunes/compiler/compiler.js +5 -0
  9. package/lunes/engine/engine.js +16 -0
  10. package/lunes/engine/listen.js +61 -0
  11. package/lunes/engine/position/get-position.js +35 -0
  12. package/lunes/hydrate-menu.js +34 -0
  13. package/lunes/importmap.js +9 -0
  14. package/lunes/lunes.js +102 -0
  15. package/lunes/menu.css +65 -0
  16. package/lunes/render/render.js +16 -0
  17. package/lunes/render/rules/README.md +3 -0
  18. package/lunes/render/rules/build-menu/fixture/build-menu-fix.js +22 -0
  19. package/lunes/render/rules/build-menu/fixture/build-menu.js +9 -0
  20. package/lunes/render/rules/build-menu/fixture/no-cursor-fix.js +9 -0
  21. package/lunes/render/rules/build-menu/fixture/no-cursor.js +9 -0
  22. package/lunes/render/rules/build-menu/index.js +132 -0
  23. package/lunes/render/rules/index.js +5 -0
  24. package/lunes/state/convert-menu-to-state/convert-menu-to-state.js +21 -0
  25. package/lunes/state/rules/README.md +5 -0
  26. package/lunes/state/rules/add-cursor/fixture/add-cursor.js +9 -0
  27. package/lunes/state/rules/add-cursor/fixture/down-fix.js +9 -0
  28. package/lunes/state/rules/add-cursor/fixture/down.js +9 -0
  29. package/lunes/state/rules/add-cursor/fixture/up-fix.js +9 -0
  30. package/lunes/state/rules/add-cursor/fixture/up.js +9 -0
  31. package/lunes/state/rules/add-cursor/index.js +34 -0
  32. package/lunes/state/rules/apply-visibility/fixture/apply-visibility-fix.js +9 -0
  33. package/lunes/state/rules/apply-visibility/fixture/apply-visibility.js +9 -0
  34. package/lunes/state/rules/apply-visibility/index.js +29 -0
  35. package/lunes/state/rules/cursor.js +77 -0
  36. package/lunes/state/rules/move-cursor/fixture/move-cursor-fix.js +9 -0
  37. package/lunes/state/rules/move-cursor/fixture/move-cursor.js +9 -0
  38. package/lunes/state/rules/move-cursor/index.js +49 -0
  39. package/lunes/state/state.js +34 -0
  40. package/menu/addons/set-position/set-position.js +1 -1
  41. package/menu/hydrate-menu.js +0 -2
  42. package/menu/importmap.js +1 -1
  43. package/nemo/addons/escape/escape.js +3 -9
  44. package/nemo/hydrate-menu.js +0 -2
  45. package/nemo/importmap.js +1 -1
  46. package/nemo/rules/index.js +0 -16
  47. package/package.json +3 -2
  48. package/nemo/addons/up.json +0 -5
  49. package/nemo/rules/hide-submenu/fixture/hide-submenu-fix.js +0 -17
  50. package/nemo/rules/hide-submenu/fixture/hide-submenu.js +0 -18
  51. package/nemo/rules/hide-submenu/fixture/no-menu.js +0 -16
  52. package/nemo/rules/hide-submenu/fixture/show-submenu.js +0 -18
  53. package/nemo/rules/hide-submenu/index.js +0 -35
  54. package/nemo/rules/menu/fixture/hide-menu-fix.js +0 -3
  55. package/nemo/rules/menu/fixture/hide-menu.js +0 -3
  56. package/nemo/rules/menu/fixture/no-menu.js +0 -3
  57. package/nemo/rules/menu/fixture/show-menu-fix.js +0 -1
  58. package/nemo/rules/menu/fixture/show-menu.js +0 -1
  59. package/nemo/rules/menu/index.js +0 -40
  60. package/nemo/rules/select/fixture/no-current.js +0 -8
  61. package/nemo/rules/select/fixture/no-data-name.js +0 -4
  62. package/nemo/rules/select/fixture/no-index.js +0 -8
  63. package/nemo/rules/select/fixture/no-next.js +0 -5
  64. package/nemo/rules/select/fixture/no-parent.js +0 -2
  65. package/nemo/rules/select/fixture/select-fix.js +0 -8
  66. package/nemo/rules/select/fixture/select.js +0 -8
  67. package/nemo/rules/select/fixture/submenu-fix.js +0 -17
  68. package/nemo/rules/select/fixture/submenu.js +0 -18
  69. package/nemo/rules/select/fixture/wrong-data-name.js +0 -4
  70. package/nemo/rules/select/index.js +0 -94
  71. package/nemo/rules/set-position/fixture/no-style-fix.js +0 -2
  72. package/nemo/rules/set-position/fixture/no-style.js +0 -2
  73. package/nemo/rules/set-position/fixture/not-menu.js +0 -2
  74. package/nemo/rules/set-position/fixture/same-position.js +0 -2
  75. package/nemo/rules/set-position/fixture/set-position-fix.js +0 -2
  76. package/nemo/rules/set-position/fixture/set-position.js +0 -2
  77. package/nemo/rules/set-position/index.js +0 -83
  78. package/nemo/rules/submenu/fixture/last-selected-fix.js +0 -53
  79. package/nemo/rules/submenu/fixture/last-selected.js +0 -54
  80. package/nemo/rules/submenu/fixture/li.js +0 -3
  81. package/nemo/rules/submenu/fixture/no-submenu-selected.js +0 -17
  82. package/nemo/rules/submenu/fixture/no-submenu.js +0 -54
  83. package/nemo/rules/submenu/fixture/not-menu.js +0 -17
  84. package/nemo/rules/submenu/fixture/submenu-fix.js +0 -53
  85. package/nemo/rules/submenu/fixture/submenu.js +0 -54
  86. package/nemo/rules/submenu/index.js +0 -80
  87. package/nemo/rules/unselect-all/fixture/hide.js +0 -8
  88. package/nemo/rules/unselect-all/fixture/nested-fix.js +0 -17
  89. package/nemo/rules/unselect-all/fixture/nested.js +0 -18
  90. package/nemo/rules/unselect-all/fixture/no-data-name.js +0 -4
  91. package/nemo/rules/unselect-all/fixture/no-parent.js +0 -2
  92. package/nemo/rules/unselect-all/fixture/unselect-all-fix.js +0 -8
  93. package/nemo/rules/unselect-all/fixture/unselect-all.js +0 -8
  94. package/nemo/rules/unselect-all/index.js +0 -51
  95. package/nemo/rules/unselect-all-submenu-items/fixture/show-submenu-fix.js +0 -33
  96. package/nemo/rules/unselect-all-submenu-items/fixture/show-submenu.js +0 -34
  97. package/nemo/rules/unselect-all-submenu-items/fixture/unselect-all-submenu-items-fix.js +0 -33
  98. package/nemo/rules/unselect-all-submenu-items/fixture/unselect-all-submenu-items.js +0 -33
  99. package/nemo/rules/unselect-all-submenu-items/index.js +0 -30
  100. package/nemo/rules/unselect-wrongly-selected/fixture/hide.js +0 -17
  101. package/nemo/rules/unselect-wrongly-selected/fixture/unselect-wrongly-selected-fix.js +0 -17
  102. package/nemo/rules/unselect-wrongly-selected/fixture/unselect-wrongly-selected.js +0 -17
  103. package/nemo/rules/unselect-wrongly-selected/index.js +0 -39
  104. /package/{menu/addons/set-position → lunes/engine/position}/calculate.js +0 -0
package/ChangeLog CHANGED
@@ -1,3 +1,30 @@
1
+ 2026.05.27, v2.0.4
2
+
3
+ fix:
4
+ - 79ad4aa aleman: menu: rm afterHydrate
5
+ - 98c1d80 example: menu
6
+
7
+ feature:
8
+ - fe3cf8c lunes: Escape
9
+ - b592739 lunes: apply-visibility: add
10
+ - f2b6589 lunes: render
11
+ - 17ac3a2 lunes: event loop
12
+ - af911e2 lunes: hydrate
13
+ - ed3bf43 lunes: cursor
14
+ - 6408584 lunes: build-menu: open/close -> on/off
15
+
16
+ 2026.05.24, v2.0.3
17
+
18
+ feature:
19
+ - 5afafd1 importmap: @putout/bundle@5.5.0
20
+ - 1c86e51 lunes: move-cursor: options: cursor
21
+ - d862a0e lunes: build-menu: position
22
+ - 54353ab lunes: build-menu: add
23
+ - f2f2e3a aleman: lunes: put cursor to AST
24
+ - 0274408 lunes: rules -> state
25
+ - d2a55a8 aleman: lunes: add
26
+ - ac3c8a1 nemo: rules: simplify
27
+
1
28
  2026.05.20, v2.0.2
2
29
 
3
30
  feature:
@@ -0,0 +1,3 @@
1
+ <script src="index.js" type="module"></script>
2
+
3
+ <body data-name="lunes"></body>
@@ -0,0 +1,22 @@
1
+ import {createMenu} from '../../lunes/lunes.js';
2
+
3
+ const menuData = {
4
+ hello: () => alert('x'),
5
+ world: () => alert('y'),
6
+ };
7
+
8
+ const options = {
9
+ name: 'menu',
10
+ infiniteScroll: true,
11
+ };
12
+
13
+ const {name} = document.body.dataset;
14
+ const menu = await createMenu(name, options, menuData);
15
+
16
+ globalThis.addEventListener('keydown', (event) => {
17
+ if (event.key === 'F9')
18
+ menu.show();
19
+
20
+ if (event.key === 'Escape')
21
+ menu.hide();
22
+ });
@@ -1 +1,2 @@
1
1
  <script src="index.js" type="module"></script>
2
+ <body data-name="aleman-example"></body>
@@ -1,8 +1,19 @@
1
- import {createMenu} from '../nemo/nemo.js';
1
+ import {createMenu} from '../../menu/menu.js';
2
2
 
3
3
  const menuData = {
4
4
  hello: () => alert('x'),
5
5
  world: () => alert('y'),
6
6
  };
7
7
 
8
- await createMenu(document.body, {}, menuData);
8
+ const options = {
9
+ name: 'menu',
10
+ infiniteScroll: true,
11
+ };
12
+
13
+ const {name} = document.body.dataset;
14
+ const menu = await createMenu(name, options, menuData);
15
+
16
+ globalThis.addEventListener('keydown', (event) => {
17
+ if (event.key === 'F9')
18
+ menu.show();
19
+ });
@@ -0,0 +1,7 @@
1
+ import up from './up.json' with {
2
+ type: 'json',
3
+ };
4
+
5
+ export const actions = [
6
+ up,
7
+ ];
@@ -0,0 +1,5 @@
1
+ {
2
+ "keys": ["ArrowUp"],
3
+ "preventDefault": true,
4
+ "operations": ["up"]
5
+ }
@@ -0,0 +1,5 @@
1
+ export const createCompiler = (element, {commit, render}) => (operation, cursor) => {
2
+ const state = commit(operation);
3
+
4
+ element.innerHTML = render(state, cursor);
5
+ };
@@ -0,0 +1,16 @@
1
+ import {listen} from './listen.js';
2
+
3
+ export const createLoop = (element, {actions, compile}) => {
4
+ setTimeout(async () => {
5
+ await loop({
6
+ actions,
7
+ compile,
8
+ });
9
+ });
10
+ };
11
+
12
+ const loop = async ({actions, compile}) => {
13
+ for await (const [operation, cursor] of listen(actions)) {
14
+ compile(operation, cursor);
15
+ }
16
+ };
@@ -0,0 +1,61 @@
1
+ export const listen = (events = ['click'], overrides = {}) => ({
2
+ [Symbol.asyncIterator]() {
3
+ const {
4
+ addEventListener = globalThis.addEventListener,
5
+ removeEventListener = globalThis.removeEventListener,
6
+ } = overrides;
7
+
8
+ const queue = [];
9
+ let resolveNext;
10
+
11
+ const handlers = events.map((event) => {
12
+ const fn = () => {
13
+ if (resolveNext) {
14
+ resolveNext({
15
+ value: event,
16
+ done: false,
17
+ });
18
+ resolveNext = null;
19
+
20
+ return;
21
+ }
22
+
23
+ queue.push(event);
24
+ };
25
+
26
+ addEventListener(event, fn);
27
+
28
+ return {
29
+ event,
30
+ fn,
31
+ };
32
+ });
33
+
34
+ const cleanup = () => {
35
+ for (const h of handlers) {
36
+ removeEventListener(h.event, h.fn);
37
+ }
38
+ };
39
+
40
+ return {
41
+ next() {
42
+ if (queue.length)
43
+ return Promise.resolve({
44
+ value: queue.shift(),
45
+ done: false,
46
+ });
47
+
48
+ return new Promise((resolve) => {
49
+ resolveNext = resolve;
50
+ });
51
+ },
52
+
53
+ return() {
54
+ cleanup();
55
+ return Promise.resolve({
56
+ done: true,
57
+ });
58
+ },
59
+ };
60
+ },
61
+ });
@@ -0,0 +1,35 @@
1
+ import {calculate} from './calculate.js';
2
+
3
+ export const getPosition = (name, event) => {
4
+ const element = document.querySelector(`[data-name="${name}"]`);
5
+ const heightMenu = getMenuHeight(element);
6
+ const widthMenu = getMenuWidth(element);
7
+ const {innerHeight, innerWidth} = globalThis;
8
+
9
+ const {x, y} = calculate(event, {
10
+ heightMenu,
11
+ widthMenu,
12
+ innerWidth,
13
+ innerHeight,
14
+ });
15
+
16
+ const left = x;
17
+ const top = y;
18
+
19
+ return {
20
+ position: {
21
+ left,
22
+ top,
23
+ },
24
+ };
25
+ };
26
+
27
+ function getMenuHeight(element) {
28
+ const {height} = getComputedStyle(element);
29
+ return parseInt(height, 10);
30
+ }
31
+
32
+ function getMenuWidth(element) {
33
+ const {width} = getComputedStyle(element);
34
+ return parseInt(width, 10);
35
+ }
@@ -0,0 +1,34 @@
1
+ import {createState} from './state/state.js';
2
+ import {render} from './render/render.js';
3
+ import {actions} from './actions/index.js';
4
+ import {createLoop} from './engine/engine.js';
5
+ import {createCompiler} from './compiler/compiler.js';
6
+
7
+ export const hydrateMenu = (elementName, {hydrateElement, menu}) => {
8
+ const {commit} = createState(menu);
9
+ const compile = createCompiler(hydrateElement, {
10
+ commit,
11
+ render,
12
+ });
13
+
14
+ createLoop(hydrateElement, {
15
+ compile,
16
+ actions,
17
+ });
18
+
19
+ return {
20
+ show: (top = 0, left = 0) => {
21
+ compile('show', {
22
+ position: {
23
+ top,
24
+ left,
25
+ },
26
+ });
27
+ },
28
+ hide: () => {
29
+ compile('hide', {
30
+ position: {},
31
+ });
32
+ },
33
+ };
34
+ };
@@ -0,0 +1,9 @@
1
+ export const importmap = {
2
+ imports: {
3
+ 'putout': 'https://esm.sh/@putout/bundle@5.5.0',
4
+ '@putout/processor-html': 'https://esm.sh/@putout/processor-html',
5
+ 'fullstore': 'https://esm.sh/fullstore',
6
+ 'jessy': 'https://esm.sh/jessy',
7
+ '#aleman': '../aleman/aleman.js',
8
+ },
9
+ };
package/lunes/lunes.js ADDED
@@ -0,0 +1,102 @@
1
+ const {stringify} = JSON;
2
+
3
+ export const createMenu = async (elementName, options, menu) => {
4
+ options.name = options.name || 'menu';
5
+ const {name} = options;
6
+
7
+ const hydrateElement = createHydrate(name);
8
+
9
+ await createMap();
10
+ await loadStyle();
11
+
12
+ const {hydrateMenu} = await import('./hydrate-menu.js');
13
+
14
+ return hydrateMenu(elementName, {
15
+ hydrateElement,
16
+ options,
17
+ menu,
18
+ });
19
+ };
20
+
21
+ async function loadStyle() {
22
+ const name = 'aleman-menu-style';
23
+
24
+ if (findByName(name))
25
+ return;
26
+
27
+ const [error] = await tryToCatch(importCSS);
28
+
29
+ if (!error)
30
+ return;
31
+
32
+ createLink();
33
+ }
34
+
35
+ function createLink() {
36
+ const link = document.createElement('link');
37
+
38
+ link.rel = 'stylesheet';
39
+ link.href = new URL('menu.css', import.meta.url).pathname;
40
+ document.head.appendChild(link);
41
+ }
42
+
43
+ async function importCSS() {
44
+ const content = await import('./menu.css', {
45
+ with: {
46
+ type: 'css',
47
+ },
48
+ });
49
+
50
+ const style = document.createElement('style');
51
+
52
+ style.dataset.name = name;
53
+
54
+ for (const rule of content.default.cssRules) {
55
+ style.innerHTML += rule.cssText;
56
+ }
57
+
58
+ document.head.appendChild(style);
59
+ }
60
+
61
+ function createHydrate(name) {
62
+ const hydrateElement = findByName(name);
63
+
64
+ if (hydrateElement)
65
+ return hydrateElement;
66
+
67
+ const section = document.createElement('section');
68
+
69
+ section.dataset.name = `aleman-hydrate-${name}`;
70
+ section.innerHTML = `<ul data-name="${name}" class="menu menu-hidden"></ul>`;
71
+ document.body.append(section);
72
+
73
+ return section;
74
+ }
75
+
76
+ async function createMap() {
77
+ const name = 'aleman-importmap';
78
+
79
+ if (findByName(name))
80
+ return;
81
+
82
+ const script = document.createElement('script');
83
+
84
+ const {importmap} = await import('./importmap.js');
85
+
86
+ script.dataset.name = name;
87
+ script.type = 'importmap';
88
+ script.innerHTML = stringify(importmap, null, 4);
89
+ document.body.append(script);
90
+ }
91
+
92
+ function findByName(name) {
93
+ return document.querySelector(`[data-name=${name}]`);
94
+ }
95
+
96
+ async function tryToCatch(fn, args) {
97
+ try {
98
+ return [null, await fn(...args)];
99
+ } catch(error) {
100
+ return [error];
101
+ }
102
+ }
package/lunes/menu.css ADDED
@@ -0,0 +1,65 @@
1
+ .menu-hidden {
2
+ display: none;
3
+ }
4
+
5
+ .menu {
6
+ position : absolute;
7
+ z-index : 2;
8
+ padding : 0;
9
+ font-size : 14px;
10
+ list-style : none;
11
+ color : #222;
12
+ background : rgb(255 255 255 / 90%);
13
+ border-color : rgb(49 123 249 / 40%);
14
+ border-style : solid;
15
+ border-width : 1px;
16
+ border-radius : 5px;
17
+ -webkit-user-select : none;
18
+ -moz-user-select : none;
19
+ -ms-user-select : none;
20
+ user-select : none;
21
+ }
22
+
23
+ .menu-button {
24
+ background: white;
25
+ border: 0;
26
+ }
27
+
28
+ .menu-item {
29
+ position: relative;
30
+ padding: 3px 20px;
31
+ white-space: nowrap;
32
+ }
33
+
34
+ .menu-item::after {
35
+ content: attr(data-key);
36
+ float: right;
37
+ }
38
+
39
+ .menu-item:hover, .menu-item-selected {
40
+ background-color: #e3f2ff;
41
+ }
42
+
43
+ .menu-item > .menu {
44
+ top: 0;
45
+ left: 100%;
46
+ display: none;
47
+ }
48
+
49
+ .menu-submenu-show > .menu, .menu-item:hover > .menu {
50
+ display: block;
51
+ }
52
+
53
+ .menu-submenu > label::after {
54
+ display: block;
55
+ float: right;
56
+ width: 0;
57
+ height: 0;
58
+ margin-top: 3px;
59
+ margin-right: -10px;
60
+ border-color: transparent;
61
+ border-left-color: rgb(49 123 249);
62
+ border-style: solid;
63
+ border-width: 5px 0 5px 5px;
64
+ content: ' ';
65
+ }
@@ -0,0 +1,16 @@
1
+ import {putout} from 'putout';
2
+ import {merge} from '@putout/processor-html';
3
+ import * as buildMenu from './rules/build-menu/index.js';
4
+
5
+ export const render = (state, options) => {
6
+ const {code} = putout(state, {
7
+ rules: {
8
+ 'lunes/build-menu': ['on', options],
9
+ },
10
+ plugins: [
11
+ ['lunes/build-menu', buildMenu],
12
+ ],
13
+ });
14
+
15
+ return merge('', [code]);
16
+ };
@@ -0,0 +1,3 @@
1
+ # Rules
2
+
3
+ - [build-menu](https://putout.cloudcmd.io/#/gist/8330c5ff0203b109f9e77c992b133d0c/ba4f1deb28448d33df5dee1647297178864c5741)
@@ -0,0 +1,22 @@
1
+ <ul className="menu" style="left: 100px; top: 20px">
2
+ <li data-name="menu-item" className="menu-item">
3
+ <label>view</label>
4
+ </li>
5
+ <li data-name="menu-item" className="menu-item">
6
+ <label>edit</label>
7
+ </li>
8
+ <li data-name="menu-item" className="menu-item">
9
+ <label>rename</label>
10
+ </li>
11
+ <li data-name="menu-item" className="menu-item menu-item-selected">
12
+ <label>new</label>
13
+ <ul className="menu">
14
+ <li data-name="menu-item" className="menu-item">
15
+ <label>file</label>
16
+ </li>
17
+ <li data-name="menu-item" className="menu-item">
18
+ <label>directory</label>
19
+ </li>
20
+ </ul>
21
+ </li>
22
+ </ul>;
@@ -0,0 +1,9 @@
1
+ ['on', {
2
+ view,
3
+ edit,
4
+ rename,
5
+ new: ['on', 'cursor', {
6
+ file,
7
+ directory,
8
+ }],
9
+ }];
@@ -0,0 +1,9 @@
1
+ ['on', 'no-cursor', {
2
+ view,
3
+ edit,
4
+ rename,
5
+ new: ['off', {
6
+ file,
7
+ directory,
8
+ }],
9
+ }];
@@ -0,0 +1,9 @@
1
+ ['on', 'no-cursor', {
2
+ view,
3
+ edit,
4
+ rename,
5
+ new: ['off', {
6
+ file,
7
+ directory,
8
+ }],
9
+ }];
@@ -0,0 +1,132 @@
1
+ import {
2
+ template,
3
+ types,
4
+ operator,
5
+ } from 'putout';
6
+
7
+ const {
8
+ isExpressionStatement,
9
+ isObjectProperty,
10
+ jsxText,
11
+ isArrayExpression,
12
+ } = types;
13
+
14
+ const {
15
+ replaceWith,
16
+ removeClassName,
17
+ setAttributeValue,
18
+ } = operator;
19
+
20
+ const isOn = ({node}) => {
21
+ if (isObjectProperty(node))
22
+ return node.value.value;
23
+
24
+ return node.elements[0].value === 'on';
25
+ };
26
+
27
+ const isSelected = ({node}) => {
28
+ if (isObjectProperty(node))
29
+ return node.value.value;
30
+
31
+ return node.elements[1].value === 'cursor';
32
+ };
33
+
34
+ export const report = () => `Build menu`;
35
+ export const include = () => [
36
+ 'ObjectProperty',
37
+ 'ArrayExpression',
38
+ ];
39
+
40
+ const createMenuItem = (path) => {
41
+ const {name} = path.node.key;
42
+ const valuePath = path.get('value');
43
+
44
+ if (isArrayExpression(valuePath)) {
45
+ const objectPath = valuePath.get('elements').at(-1);
46
+ const children = [];
47
+
48
+ for (const prop of objectPath.get('properties').map(createMenuItem)) {
49
+ children.push(prop);
50
+ children.push(jsxText('\n'));
51
+ }
52
+
53
+ const submenu = createSubmenu(valuePath);
54
+
55
+ submenu.children[3].children = [
56
+ jsxText('\n'),
57
+ ...children,
58
+ ];
59
+
60
+ return submenu;
61
+ }
62
+
63
+ const classSelected = isSelected(path) ? 'menu-item-selected' : '';
64
+
65
+ const node = template.ast.fresh(`
66
+ <li data-name="menu-item" className="menu-item${classSelected}">
67
+ <label>${name}</label>
68
+ </li>
69
+ `);
70
+
71
+ return node;
72
+ };
73
+
74
+ const createUL = (path) => {
75
+ const node = template.ast.fresh(`
76
+ <ul className="menu menu-hidden"></ul>
77
+ `);
78
+
79
+ if (isOn(path))
80
+ removeClassName(node, 'menu-hidden');
81
+
82
+ return node;
83
+ };
84
+
85
+ const createSubmenu = (path) => {
86
+ const {name} = path.parentPath.node.key;
87
+ const menuHidden = isOn(path) ? '' : ' menu-hidden';
88
+ const classSelected = isSelected(path) ? ' menu-item-selected' : '';
89
+
90
+ const node = template.ast.fresh(`
91
+ <li data-name="menu-item" className="menu-item${classSelected}">
92
+ <label>${name}</label>
93
+ <ul className="menu${menuHidden}"></ul>
94
+ </li>
95
+ `);
96
+
97
+ return node;
98
+ };
99
+
100
+ export const fix = (path, {options}) => {
101
+ const {position} = options;
102
+ const {top, left} = position;
103
+
104
+ if (isObjectProperty(path)) {
105
+ replaceWith(path, createMenuItem(path));
106
+ return;
107
+ }
108
+
109
+ if (isArrayExpression(path)) {
110
+ const ul = createUL(path);
111
+
112
+ if (isExpressionStatement(path.parentPath)) {
113
+ replaceWith(path, ul);
114
+ setAttributeValue(ul, 'style', `left: ${left}px; top: ${top}px`);
115
+ }
116
+
117
+ const {properties} = path.node.elements.at(-1);
118
+ const children = [];
119
+
120
+ for (const prop of properties) {
121
+ children.push(prop);
122
+ children.push(jsxText('\n'));
123
+ }
124
+
125
+ ul.children = [
126
+ jsxText('\n'),
127
+ ...children,
128
+ ];
129
+
130
+ return;
131
+ }
132
+ };
@@ -0,0 +1,5 @@
1
+ import * as buildMenu from './build-menu/index.js';
2
+
3
+ export const rules = {
4
+ 'build-menu': buildMenu,
5
+ };
@@ -0,0 +1,21 @@
1
+ const {entries} = Object;
2
+ const isObject = (a) => a && typeof a === 'object';
3
+
4
+ export function convertMenuToState(menu, {submenu} = {}) {
5
+ const result = [];
6
+
7
+ for (const [key, value] of entries(menu)) {
8
+ if (!isObject(value)) {
9
+ result.push(key);
10
+ continue;
11
+ }
12
+
13
+ result.push(`${key}: ${convertMenuToState(value, {
14
+ submenu: true,
15
+ })}`);
16
+ }
17
+
18
+ const noCursor = submenu ? '' : `'no-cursor', `;
19
+
20
+ return `['off', ${noCursor} {${result.join(', ')}}]`;
21
+ }
@@ -0,0 +1,5 @@
1
+ ## Rules
2
+
3
+ - [add-cursor](https://putout.cloudcmd.io/#/gist/0d0be0b7d19949be48c4122416654936/0cf8e1c512a0503f8c4bd0abf2af221b6680630b);
4
+ - [move-cursor](https://putout.cloudcmd.io/#/gist/54ff76b9673e0caf216b0f0f33d9f8bb/dad16ba0f861073a3ab7549e553dd2b97d252729);
5
+ - [apply-visibility](https://putout.cloudcmd.io/#/gist/7b97aaec7fcc518de4f6eef4ecc3dd58/f38db01fb9af051d49bd114bfb7bb04d2298c501);