flexdesk 0.17.18 → 0.18.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.
Files changed (118) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +1 -0
  3. package/css/flexdesk.css +7544 -5916
  4. package/css/reset.css +1 -1
  5. package/css/tokens.css +23 -1
  6. package/dist/charts.js +10 -6
  7. package/dist/charts.js.map +1 -1
  8. package/dist/chunk-5W4EEKSK.js +778 -0
  9. package/dist/chunk-5W4EEKSK.js.map +7 -0
  10. package/dist/chunk-DNJ3N4T4.js +82 -0
  11. package/dist/chunk-DNJ3N4T4.js.map +7 -0
  12. package/dist/{chunk-REJ5HLIM.js → chunk-HYJYYEPK.js} +2 -2
  13. package/dist/{chunk-7U4IB65H.js → chunk-LLBJ2SJH.js} +5 -3
  14. package/dist/{chunk-7U4IB65H.js.map → chunk-LLBJ2SJH.js.map} +1 -1
  15. package/dist/{chunk-7OL6F2AM.js → chunk-P74F6I6P.js} +6 -4
  16. package/dist/{chunk-7OL6F2AM.js.map → chunk-P74F6I6P.js.map} +1 -1
  17. package/dist/chunk-PAI2ZUWV.js +260 -0
  18. package/dist/chunk-PAI2ZUWV.js.map +7 -0
  19. package/dist/chunk-Q7BALEPX.js +527 -0
  20. package/dist/chunk-Q7BALEPX.js.map +7 -0
  21. package/dist/chunk-QXAOTGCC.js +40 -0
  22. package/dist/chunk-QXAOTGCC.js.map +7 -0
  23. package/dist/{chunk-XJNADRGR.js → chunk-RKVYIXDC.js} +664 -91
  24. package/dist/chunk-RKVYIXDC.js.map +7 -0
  25. package/dist/{chunk-2Z2ENHR3.js → chunk-U5GKJUOC.js} +3 -3
  26. package/dist/{chunk-2Z2ENHR3.js.map → chunk-U5GKJUOC.js.map} +1 -1
  27. package/dist/chunk-UKP22RUG.js +219 -0
  28. package/dist/chunk-UKP22RUG.js.map +7 -0
  29. package/dist/chunk-VLOAI3HS.js +43 -0
  30. package/dist/chunk-VLOAI3HS.js.map +7 -0
  31. package/dist/{chunk-WUFLGETN.js → chunk-WVFGV5FT.js} +2 -39
  32. package/dist/{chunk-WUFLGETN.js.map → chunk-WVFGV5FT.js.map} +3 -3
  33. package/dist/{chunk-PGZ4EHSE.js → chunk-ZL3VKINP.js} +41 -390
  34. package/dist/chunk-ZL3VKINP.js.map +7 -0
  35. package/dist/editor.js +3 -2
  36. package/dist/editor.js.map +1 -1
  37. package/dist/flexdesk.css +7544 -5916
  38. package/dist/flow.js +29 -97
  39. package/dist/flow.js.map +3 -3
  40. package/dist/git.js +1356 -0
  41. package/dist/git.js.map +7 -0
  42. package/dist/reset.css +1 -1
  43. package/dist/tiles.js +20 -15
  44. package/dist/tiles.js.map +2 -2
  45. package/dist/tokens.css +23 -1
  46. package/dist/widgets.js +3967 -1188
  47. package/dist/widgets.js.map +4 -4
  48. package/dist/wm.js +120 -32
  49. package/dist/wm.js.map +2 -2
  50. package/docs/README.md +9 -4
  51. package/docs/best-practices.md +2 -2
  52. package/docs/components/chat-shell.md +198 -0
  53. package/docs/components/conversation.md +10 -0
  54. package/docs/components/filter-builder.md +246 -0
  55. package/docs/components/forms-and-dialogs.md +22 -0
  56. package/docs/components/git.md +355 -0
  57. package/docs/components/input-fields.md +167 -2
  58. package/docs/components/keymap.md +8 -0
  59. package/docs/components/menus.md +82 -2
  60. package/docs/components/panels-and-desktops.md +16 -5
  61. package/docs/components/shell.md +104 -1
  62. package/docs/components/workspace-picker.md +149 -0
  63. package/docs/concepts/building-and-packaging.md +2 -2
  64. package/docs/concepts/packages-and-entry-points.md +14 -13
  65. package/docs/concepts/state-guard.md +3 -3
  66. package/docs/quickstart.md +1 -1
  67. package/git.js +13 -0
  68. package/package.json +6 -1
  69. package/src/flow/kit/dom.js +28 -119
  70. package/src/flow/kit/step_picker.js +1 -1
  71. package/src/flow/kit/value_picker.js +1 -1
  72. package/src/flow/outline/menu.js +1 -1
  73. package/src/git/diff_viewer.js +212 -0
  74. package/src/git/diff_window.js +51 -0
  75. package/src/git/git_graph.js +401 -0
  76. package/src/git/git_ops.js +61 -0
  77. package/src/git/git_panel.js +755 -0
  78. package/src/git/index.js +9 -0
  79. package/src/git/line_diff.js +202 -0
  80. package/src/tiles/tile_base.js +5 -4
  81. package/src/tiling/desktops.js +10 -8
  82. package/src/tiling/keymap.js +6 -5
  83. package/src/tiling/kind_taxonomy.js +15 -2
  84. package/src/tiling/shell.js +124 -17
  85. package/src/tiling/tile_renderer.js +2 -3
  86. package/src/ui/components/action_dropdown.js +4 -78
  87. package/src/ui/components/anchored_layer.js +255 -0
  88. package/src/ui/components/chat_shell.js +920 -0
  89. package/src/ui/components/chat_state_machine.js +223 -0
  90. package/src/ui/components/choice_controls.js +45 -2
  91. package/src/ui/components/color_field.js +3 -2
  92. package/src/ui/components/color_list_field.js +3 -2
  93. package/src/ui/components/combobox.js +422 -0
  94. package/src/ui/components/conversation_view.js +17 -2
  95. package/src/ui/components/data_table.js +2 -2
  96. package/src/ui/components/filter_builder.js +766 -0
  97. package/src/ui/components/filter_model.js +614 -0
  98. package/src/ui/components/managed_window.js +24 -47
  99. package/src/ui/components/modal.js +79 -109
  100. package/src/ui/components/option_list.js +160 -0
  101. package/src/ui/components/popover.js +94 -0
  102. package/src/ui/components/recent_items.js +169 -0
  103. package/src/ui/components/select.js +322 -0
  104. package/src/ui/components/tag_input.js +77 -8
  105. package/src/ui/components/workspace_picker.js +1155 -0
  106. package/src/ui/utils/fire_event.js +17 -0
  107. package/src/ui/utils/focusable.js +25 -0
  108. package/src/ui/utils/modal_host.js +27 -0
  109. package/widgets.js +12 -2
  110. package/css/base.css +0 -10547
  111. package/css/overrides.css +0 -2596
  112. package/dist/chunk-7JI7HHSY.js +0 -107
  113. package/dist/chunk-7JI7HHSY.js.map +0 -7
  114. package/dist/chunk-MIW7D6WC.js +0 -526
  115. package/dist/chunk-MIW7D6WC.js.map +0 -7
  116. package/dist/chunk-PGZ4EHSE.js.map +0 -7
  117. package/dist/chunk-XJNADRGR.js.map +0 -7
  118. /package/dist/{chunk-REJ5HLIM.js.map → chunk-HYJYYEPK.js.map} +0 -0
@@ -9,10 +9,7 @@
9
9
  * Adds scrolling when the menu would exceed available vertical space.
10
10
  */
11
11
 
12
- /** Viewport margin in px — dropdown stays this far from edges. */
13
- const EDGE_MARGIN = 8;
14
- /** Gap between trigger and dropdown in px. */
15
- const GAP = 8;
12
+ import { positionAnchored } from './anchored_layer.js';
16
13
 
17
14
  /**
18
15
  * ActionDropdown - A reusable dropdown component with smart auto-positioning.
@@ -371,11 +368,8 @@ export class ActionDropdown {
371
368
  // =========================================================================
372
369
 
373
370
  /**
374
- * Position a dropdown menu relative to a trigger element.
375
- * Uses `position: fixed` to avoid overflow clipping.
376
- * Picks above/below based on available space, aligns left/right edge,
377
- * and applies `max-height` + `overflow-y: auto` when the menu is taller
378
- * than the available space.
371
+ * Position a dropdown menu relative to a trigger element: `positionAnchored`
372
+ * (`anchored_layer.js`), the one placement every floating panel uses.
379
373
  *
380
374
  * @param {HTMLElement} trigger - The element the dropdown opens from
381
375
  * @param {HTMLElement} menu - The dropdown menu element to position
@@ -386,75 +380,7 @@ export class ActionDropdown {
386
380
  */
387
381
  static position(trigger, menu, { align = 'start' } = {}) {
388
382
  if (!trigger || !menu) return;
389
-
390
- // Reset any prior inline position so natural size can be measured.
391
- Object.assign(menu.style, {
392
- position: 'fixed',
393
- top: 'auto',
394
- bottom: 'auto',
395
- left: 'auto',
396
- right: 'auto',
397
- maxHeight: '',
398
- overflowY: ''
399
- });
400
-
401
- const triggerRect = trigger.getBoundingClientRect();
402
- const menuRect = menu.getBoundingClientRect();
403
- const vw = window.innerWidth;
404
- const vh = window.innerHeight;
405
-
406
- // ── Vertical: prefer below, flip above if insufficient room ───────
407
- const spaceBelow = vh - triggerRect.bottom - GAP - EDGE_MARGIN;
408
- const spaceAbove = triggerRect.top - GAP - EDGE_MARGIN;
409
-
410
- let top;
411
- let maxHeight;
412
-
413
- if (menuRect.height <= spaceBelow) {
414
- // Fits below — use natural height.
415
- top = triggerRect.bottom + GAP;
416
- maxHeight = spaceBelow;
417
- } else if (menuRect.height <= spaceAbove) {
418
- // Fits above — use natural height.
419
- top = triggerRect.top - GAP - menuRect.height;
420
- maxHeight = spaceAbove;
421
- } else if (spaceBelow >= spaceAbove) {
422
- // More space below — scroll.
423
- top = triggerRect.bottom + GAP;
424
- maxHeight = spaceBelow;
425
- } else {
426
- // More space above — scroll.
427
- maxHeight = spaceAbove;
428
- top = EDGE_MARGIN;
429
- }
430
-
431
- // ── Horizontal: align with the trigger's start (left) or end (right)
432
- // edge, flip to the other edge if off-screen ──
433
- let left;
434
- if (align === 'end') {
435
- left = triggerRect.right - menuRect.width;
436
- if (left < EDGE_MARGIN) {
437
- // Align left edges instead.
438
- left = triggerRect.left;
439
- }
440
- } else {
441
- left = triggerRect.left;
442
- if (left + menuRect.width > vw - EDGE_MARGIN) {
443
- // Align right edges instead.
444
- left = triggerRect.right - menuRect.width;
445
- }
446
- }
447
-
448
- // Still off-screen? Clamp to edges.
449
- left = Math.max(EDGE_MARGIN, Math.min(left, vw - menuRect.width - EDGE_MARGIN));
450
-
451
- Object.assign(menu.style, {
452
- position: 'fixed',
453
- top: `${Math.round(top)}px`,
454
- left: `${Math.round(left)}px`,
455
- maxHeight: `${Math.round(maxHeight)}px`,
456
- overflowY: 'auto'
457
- });
383
+ positionAnchored(trigger, menu, { align });
458
384
  }
459
385
  }
460
386
 
@@ -0,0 +1,255 @@
1
+ /**
2
+ * ONE WAY TO HANG A PANEL OFF AN ELEMENT. (0.18.0)
3
+ *
4
+ * `positionAnchored` is the placement: `ActionDropdown.position`, `CategoryPicker`,
5
+ * a tile's menu, `openPopover`, `createSelect` and `createCombobox` all place
6
+ * through it, so none of them has a second idea of what "below, else above,
7
+ * inside the window" means.
8
+ *
9
+ * `openAnchoredLayer` is the behaviour around the placement:
10
+ *
11
+ * - the panel is mounted on FlexDesk's modal host (`modalHost()`) or the anchor's
12
+ * `document.body`, never inside the anchor's parent, so no `overflow: hidden`
13
+ * ancestor clips it. It is taken off again on close: a closed layer leaves
14
+ * nothing behind to forget;
15
+ * - it is placed again when the window resizes, when anything outside it
16
+ * scrolls and when its own size changes; an anchor that has left the document
17
+ * closes it;
18
+ * - it closes on a press outside it and on Escape.
19
+ *
20
+ * LAYERS STACK, because a popover's own controls hang panels too. A select inside
21
+ * a popover is a layer whose anchor is inside the popover; that gives every rule
22
+ * below one answer:
23
+ *
24
+ * - OPENING a layer closes every open layer that does not own its anchor (so
25
+ * there is one popover at a time, and a popover's select does not close it);
26
+ * - A PRESS on what a layer owns is not outside it, and neither is one on its anchor;
27
+ * - ESCAPE goes to the top layer alone and to nothing else, or the `ManagedWindow`
28
+ * around the field would close too and take a half-filled form with it. From
29
+ * inside the panel it is heard as it bubbles out, so that the panel's content
30
+ * may answer it first (a picker climbing a level); from anywhere else, on the
31
+ * way in;
32
+ * - CLOSING a layer closes the layers above it first.
33
+ *
34
+ * WHAT A LAYER OWNS is one question with one answer, for a press and for an anchor
35
+ * alike: the panel; every layer above it; and what floats without being a layer, which
36
+ * is named here: an `ActionDropdown` menu, a `CategoryPicker`, a context menu, a modal
37
+ * window shown after the layer opened (a dialog its content opened), and whatever the
38
+ * caller's `inside` selector names. A modal already showing when the layer opened is
39
+ * UNDER it, however far down the chain of layers it was opened from: it owns none of
40
+ * that and takes no key from it. An open menu or picker, and a modal over the layer,
41
+ * get Escape first.
42
+ *
43
+ * An anchor that has left the document has nothing to hang a panel from: opening on
44
+ * one opens nothing and closes nothing, and answers `null`.
45
+ */
46
+ import { modalHost } from '../utils/modal_host.js';
47
+
48
+ /** Viewport margin in px: a placed panel stays this far from the edges. */
49
+ const EDGE_MARGIN = 8;
50
+ /** Gap between an anchor and its panel in px, unless the caller gives one. */
51
+ const GAP = 8;
52
+
53
+ const NO_BOX = { left: 0, right: 0, top: 0, bottom: 0, width: 0, height: 0 };
54
+
55
+ /**
56
+ * Place `panel` against `anchor`: `position: fixed`, so no ancestor clips it;
57
+ * under the anchor when it fits there, else above it when it fits there, else on
58
+ * the side with more room, scrolling inside (`max-height` + `overflow-y: auto`);
59
+ * kept `margin` inside the window. The client size is the window: a page
60
+ * scrollbar is not room to draw in.
61
+ *
62
+ * @param {HTMLElement|null} anchor Null places it at the window's top-left corner.
63
+ * @param {HTMLElement} panel
64
+ * @param {object} [opts]
65
+ * @param {'start'|'end'} [opts.align='start'] Which anchor edge the panel lines up
66
+ * with first: `'start'` its left, `'end'` its right. Either flips to the other
67
+ * edge when the panel would leave the window.
68
+ * @param {number} [opts.gap=8] Px between the anchor and the panel.
69
+ * @param {number} [opts.margin=8] Px kept clear of the window's edges.
70
+ * @param {boolean} [opts.matchWidth] The panel is at least as wide as the anchor.
71
+ */
72
+ export function positionAnchored(anchor, panel, { align = 'start', gap = GAP, margin = EDGE_MARGIN, matchWidth = false } = {}) {
73
+ if (!panel) return;
74
+
75
+ // Reset any prior inline position so natural size can be measured.
76
+ Object.assign(panel.style, {
77
+ position: 'fixed',
78
+ top: 'auto',
79
+ bottom: 'auto',
80
+ left: 'auto',
81
+ right: 'auto',
82
+ maxHeight: '',
83
+ overflowY: '',
84
+ });
85
+
86
+ const box = anchor?.getBoundingClientRect?.() || NO_BOX;
87
+ if (matchWidth) panel.style.minWidth = `${Math.round(box.width)}px`;
88
+ const size = panel.getBoundingClientRect();
89
+ const doc = anchor?.ownerDocument || document;
90
+ const view = doc.defaultView || window;
91
+ const vw = doc.documentElement.clientWidth || view.innerWidth;
92
+ const vh = doc.documentElement.clientHeight || view.innerHeight;
93
+
94
+ // ── Vertical: prefer below, flip above if insufficient room ───────
95
+ const spaceBelow = vh - box.bottom - gap - margin;
96
+ const spaceAbove = box.top - gap - margin;
97
+
98
+ let top;
99
+ let maxHeight;
100
+ if (size.height <= spaceBelow) {
101
+ top = box.bottom + gap;
102
+ maxHeight = spaceBelow;
103
+ } else if (size.height <= spaceAbove) {
104
+ top = box.top - gap - size.height;
105
+ maxHeight = spaceAbove;
106
+ } else if (spaceBelow >= spaceAbove) {
107
+ // More space below: scroll.
108
+ top = box.bottom + gap;
109
+ maxHeight = spaceBelow;
110
+ } else {
111
+ // More space above: scroll.
112
+ maxHeight = spaceAbove;
113
+ top = margin;
114
+ }
115
+
116
+ // ── Horizontal: the chosen edge, the other if off-screen, then clamped ──
117
+ let left;
118
+ if (align === 'end') {
119
+ left = box.right - size.width;
120
+ if (left < margin) left = box.left;
121
+ } else {
122
+ left = box.left;
123
+ if (left + size.width > vw - margin) left = box.right - size.width;
124
+ }
125
+ left = Math.max(margin, Math.min(left, vw - size.width - margin));
126
+
127
+ Object.assign(panel.style, {
128
+ position: 'fixed',
129
+ top: `${Math.round(top)}px`,
130
+ left: `${Math.round(left)}px`,
131
+ maxHeight: `${Math.round(maxHeight)}px`,
132
+ overflowY: 'auto',
133
+ });
134
+ }
135
+
136
+ /** What floats without being a layer. See the header. */
137
+ const FOREIGN_FLOATS = '.twm-action-dropdown-menu, .twm-category-picker, .twm-context-menu';
138
+ /** Which of those answer Escape themselves while open. */
139
+ const FOREIGN_ESCAPE = '.twm-action-dropdown-menu:not([hidden]), .twm-category-picker:not([hidden])';
140
+ const MODAL_WINDOW = '.twm-managed-window--modal';
141
+
142
+ /** The open layers, bottom first. */
143
+ const LAYERS = [];
144
+
145
+ const holds = (node, target) => !!node && typeof node.contains === 'function' && node.contains(target);
146
+
147
+ const shownModals = (doc) => [...doc.querySelectorAll(MODAL_WINDOW)].filter((win) => win.style.display !== 'none');
148
+
149
+ /** Is `node` in an open layer? A modal's focus trap leaves such focus alone: the panel is outside the modal's frame. */
150
+ export const isInAnchoredLayer = (node) => LAYERS.some((layer) => holds(layer.el, node));
151
+
152
+ /**
153
+ * Open `el` as a layer against `anchor`.
154
+ *
155
+ * @param {object} o
156
+ * @param {HTMLElement|null} o.anchor
157
+ * @param {HTMLElement} o.el The panel. Mounted here, removed on close; reusable.
158
+ * @param {'start'|'end'} [o.align]
159
+ * @param {number} [o.gap]
160
+ * @param {boolean} [o.matchWidth]
161
+ * @param {string} [o.inside] A selector for floating things of the caller's own:
162
+ * a press in one is not outside this layer.
163
+ * @param {boolean} [o.returnFocus=true] Closed with focus inside the panel, or on
164
+ * nothing, focus goes back to the anchor, unless the reason is `'blur'`: focus
165
+ * that left is going where it was sent.
166
+ * @param {(reason: string) => void} [o.onClose] After it is gone. The reasons are
167
+ * `'outside'`, `'escape'`, `'replaced'`, `'parent'`, `'detached'`, and whatever
168
+ * the caller passes to `close`.
169
+ * @returns {{el: HTMLElement, anchor: HTMLElement|null, place: () => void, owns: (node: Node) => boolean,
170
+ * close: (reason?: string) => void}|null}
171
+ * `null` when `anchor` has left the document: nothing opened, `onClose` will not be called.
172
+ */
173
+ export function openAnchoredLayer({ anchor = null, el, align = 'start', gap = GAP, matchWidth = false, inside = null,
174
+ returnFocus = true, onClose = null }) {
175
+ if (anchor && anchor.isConnected === false) return null;
176
+
177
+ while (LAYERS.length && !LAYERS[LAYERS.length - 1].owns(anchor)) LAYERS[LAYERS.length - 1].close('replaced');
178
+
179
+ const doc = anchor?.ownerDocument || document;
180
+ const view = doc.defaultView || window;
181
+ (modalHost() || doc.body).appendChild(el);
182
+ const modalsUnder = new Set(shownModals(doc));
183
+
184
+ let closed = false;
185
+ const place = () => {
186
+ if (closed) return;
187
+ if (anchor && anchor.isConnected === false) { layer.close('detached'); return; }
188
+ positionAnchored(anchor, el, { align, gap, matchWidth });
189
+ };
190
+
191
+ const owns = (target) => {
192
+ if (holds(el, target)) return true;
193
+ for (let i = LAYERS.indexOf(layer) + 1; i < LAYERS.length; i += 1) if (holds(LAYERS[i].el, target)) return true;
194
+ if (typeof target?.closest !== 'function') return false;
195
+ if (target.closest(FOREIGN_FLOATS)) return true;
196
+ const modal = target.closest(MODAL_WINDOW);
197
+ if (modal && !modalsUnder.has(modal)) return true;
198
+ return !!inside && !!target.closest(inside);
199
+ };
200
+ const onPress = (ev) => { if (!holds(anchor, ev.target) && !owns(ev.target)) layer.close('outside'); };
201
+ const onDocKey = (ev) => { if (!holds(el, ev.target)) onKey(ev); };
202
+ const onKey = (ev) => {
203
+ if (ev.key !== 'Escape' || ev.isComposing || ev.defaultPrevented) return;
204
+ if (LAYERS[LAYERS.length - 1] !== layer) return;
205
+ if (doc.querySelector(FOREIGN_ESCAPE)) return;
206
+ if (shownModals(doc).some((win) => !modalsUnder.has(win))) return;
207
+ ev.preventDefault();
208
+ ev.stopPropagation();
209
+ layer.close('escape');
210
+ };
211
+ const onScroll = (ev) => { if (!holds(el, ev.target)) place(); };
212
+ // Placed before a caller fills it, and it changes with a filter: placed again
213
+ // whenever its size changes, or it would run past the window's edge.
214
+ const sized = typeof ResizeObserver === 'function' ? new ResizeObserver(() => place()) : null;
215
+
216
+ const layer = {
217
+ el,
218
+ anchor,
219
+ place,
220
+ owns,
221
+ close(reason = 'closed') {
222
+ if (closed) return;
223
+ while (LAYERS[LAYERS.length - 1] !== layer) LAYERS[LAYERS.length - 1].close('parent');
224
+ closed = true;
225
+ const active = doc.activeElement;
226
+ const held = holds(el, active) || active === doc.body;
227
+ doc.removeEventListener('pointerdown', onPress, true);
228
+ doc.removeEventListener('mousedown', onPress, true);
229
+ doc.removeEventListener('keydown', onDocKey, true);
230
+ el.removeEventListener('keydown', onKey);
231
+ view.removeEventListener('resize', place);
232
+ view.removeEventListener('scroll', onScroll, true);
233
+ sized?.disconnect();
234
+ el.remove();
235
+ LAYERS.pop();
236
+ if (returnFocus && held && reason !== 'blur' && anchor?.isConnected) anchor.focus?.({ preventScroll: true });
237
+ onClose?.(reason);
238
+ },
239
+ };
240
+ LAYERS.push(layer);
241
+
242
+ // The press that OPENED it is still being dispatched; listen from the next turn.
243
+ setTimeout(() => {
244
+ if (closed) return;
245
+ doc.addEventListener('pointerdown', onPress, true);
246
+ doc.addEventListener('mousedown', onPress, true);
247
+ }, 0);
248
+ doc.addEventListener('keydown', onDocKey, true);
249
+ el.addEventListener('keydown', onKey);
250
+ view.addEventListener('resize', place);
251
+ view.addEventListener('scroll', onScroll, true);
252
+ sized?.observe(el);
253
+ place();
254
+ return layer;
255
+ }