@limetech/lime-elements 40.10.1 → 40.11.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.
@@ -103,7 +103,8 @@
103
103
  * @prop --side-panel-top-inset: Distance from the top of the positioned ancestor. Defaults to `0px`.
104
104
  * @prop --side-panel-right-inset: Distance from the right of the positioned ancestor. Defaults to `0px`.
105
105
  * @prop --side-panel-bottom-inset: Distance from the bottom of the positioned ancestor. Defaults to `0px`.
106
- * @prop --side-panel-z-index: z-index of the panel. Defaults to `6`. Set `--dropdown-z-index` and `--popover-z-index` to higher values, so that dropdowns and popovers inside the panel are displayed on top of it.
106
+ * @prop --side-panel-header-text-color: Set by the panel on the header, do not set it. Content in the `header-actions` slot can use it to match the text color of the header, which is white when the header has a `color`.
107
+ * @prop --side-panel-z-index: z-index of the panel. Defaults to `6`. The panel sets `--dropdown-z-index`, `--popover-z-index` and `--tooltip-z-index` to one more than this for everything inside it, so that dropdowns, popovers and tooltips are displayed on top of the panel. To use other values, set these properties on the panel or on an ancestor.
107
108
  */
108
109
  :host(limel-side-panel) {
109
110
  --_side-panel-width: var(--side-panel-width, 37.5rem);
@@ -112,6 +113,9 @@
112
113
  --_side-panel-right-inset: var(--side-panel-right-inset, 0px);
113
114
  --_side-panel-bottom-inset: var(--side-panel-bottom-inset, 0px);
114
115
  --_side-panel-z-index: var(--side-panel-z-index, 6);
116
+ --_side-panel-dropdown-z-index: var(--dropdown-z-index);
117
+ --_side-panel-popover-z-index: var(--popover-z-index);
118
+ --_side-panel-tooltip-z-index: var(--tooltip-z-index);
115
119
  }
116
120
 
117
121
  @keyframes show-side-panel {
@@ -157,6 +161,18 @@ section:focus {
157
161
  }
158
162
 
159
163
  .content {
164
+ --dropdown-z-index: var(
165
+ --_side-panel-dropdown-z-index,
166
+ calc(var(--_side-panel-z-index) + 1)
167
+ );
168
+ --popover-z-index: var(
169
+ --_side-panel-popover-z-index,
170
+ calc(var(--_side-panel-z-index) + 1)
171
+ );
172
+ --tooltip-z-index: var(
173
+ --_side-panel-tooltip-z-index,
174
+ calc(var(--_side-panel-z-index) + 1)
175
+ );
160
176
  display: flex;
161
177
  flex-direction: column;
162
178
  flex-grow: 1;
@@ -177,6 +193,134 @@ section:focus {
177
193
  background-color: rgb(var(--contrast-300));
178
194
  }
179
195
 
196
+ .actions {
197
+ display: flex;
198
+ align-items: center;
199
+ gap: 0.25rem;
200
+ margin-left: auto;
201
+ }
202
+ .actions > * {
203
+ appearance: none;
204
+ background: none;
205
+ border: none;
206
+ padding: 0;
207
+ margin: 0;
208
+ font: inherit;
209
+ color: inherit;
210
+ text-align: inherit;
211
+ transition: color var(--limel-clickable-transition-speed, 0.4s) ease, background-color var(--limel-clickable-transition-speed, 0.4s) ease, box-shadow var(--limel-clickable-transform-speed, 0.4s) ease, transform var(--limel-clickable-transform-speed, 0.4s) var(--limel-clickable-transform-timing-function, ease);
212
+ cursor: pointer;
213
+ color: var(--limel-theme-on-surface-color);
214
+ background-color: transparent;
215
+ }
216
+ .actions > *:hover, .actions > *:focus, .actions > *:focus-visible {
217
+ will-change: color, background-color, box-shadow, transform;
218
+ }
219
+ .actions > *:hover, .actions > *:focus-visible {
220
+ transform: translate3d(0, 0.01rem, 0);
221
+ color: var(--limel-theme-on-surface-color);
222
+ background-color: var(--lime-elevated-surface-background-color);
223
+ }
224
+ .actions > *:hover {
225
+ box-shadow: var(--button-shadow-hovered);
226
+ }
227
+ .actions > *:active {
228
+ --limel-clickable-transform-timing-function: cubic-bezier(
229
+ 0.83,
230
+ -0.15,
231
+ 0.49,
232
+ 1.16
233
+ );
234
+ transform: translate3d(0, 0.05rem, 0);
235
+ box-shadow: var(--button-shadow-pressed);
236
+ }
237
+ .actions > *:hover, .actions > *:active {
238
+ --limel-clickable-transition-speed: 0.2s;
239
+ --limel-clickable-transform-speed: 0.16s;
240
+ }
241
+ .actions > *:focus {
242
+ outline: none;
243
+ }
244
+ .actions > *:focus-visible {
245
+ outline: none;
246
+ box-shadow: var(--shadow-depth-8-focused);
247
+ }
248
+ .actions > * {
249
+ isolation: isolate;
250
+ position: relative;
251
+ display: flex;
252
+ justify-content: center;
253
+ align-items: center;
254
+ width: 1.875rem;
255
+ height: 1.875rem;
256
+ border-radius: 50%;
257
+ }
258
+ .actions > * limel-icon {
259
+ width: 1.25rem;
260
+ transition: transform 0.2s ease, color 0.2s ease;
261
+ }
262
+ .actions > * {
263
+ --_side-panel-action-background-color: rgb(var(--contrast-300));
264
+ }
265
+ .actions > *:hover, .actions > *:focus-visible {
266
+ --_side-panel-action-background-color: var(
267
+ --lime-elevated-surface-background-color
268
+ );
269
+ }
270
+ .actions > *:hover limel-icon {
271
+ color: var(--mdc-theme-primary);
272
+ }
273
+
274
+ .newTab limel-icon {
275
+ transition: transform 0.2s ease;
276
+ transform: scale(0.8);
277
+ z-index: 3;
278
+ }
279
+ .newTab:before {
280
+ z-index: 2;
281
+ }
282
+ .newTab:after {
283
+ z-index: 1;
284
+ }
285
+ .newTab:before, .newTab:after {
286
+ content: "";
287
+ transition: transform 0.2s ease, outline-color 0.2s ease;
288
+ position: absolute;
289
+ width: 0.875rem;
290
+ height: 0.875rem;
291
+ outline: 1.5px solid rgb(var(--contrast-900));
292
+ border-radius: 1px;
293
+ background-color: var(--_side-panel-action-background-color);
294
+ }
295
+ .newTab:hover limel-icon {
296
+ transform: translate3d(-0.1rem, 0.1rem, 0) scale(1);
297
+ }
298
+ .newTab:hover:before {
299
+ transform: translate3d(0.125rem, -0.125rem, 0);
300
+ outline-color: rgb(var(--contrast-1000));
301
+ box-shadow: var(--shadow-depth-8);
302
+ }
303
+ .newTab:hover:after {
304
+ transform: translate3d(-0.15rem, 0.15rem, 0) scale(0.9);
305
+ outline-color: rgb(var(--contrast-700));
306
+ }
307
+
308
+ .expand:after {
309
+ transition: transform 0.2s ease;
310
+ content: "";
311
+ position: absolute;
312
+ z-index: 1;
313
+ background-color: var(--_side-panel-action-background-color);
314
+ width: 0.75rem;
315
+ height: 0.75rem;
316
+ }
317
+ .expand:hover:after {
318
+ transform: scale(0.5);
319
+ }
320
+ .expand:hover limel-icon {
321
+ transform: scale(1.18);
322
+ }
323
+
180
324
  .close {
181
325
  appearance: none;
182
326
  background: none;
@@ -267,6 +411,37 @@ section:focus {
267
411
  transform: scale(0.6);
268
412
  }
269
413
 
414
+ .header {
415
+ --side-panel-header-text-color: var(--limel-theme-on-surface-color);
416
+ --header-top-right-left-border-radius: 0;
417
+ flex-shrink: 0;
418
+ min-height: calc(1.25rem + 1rem + 2 * 0.125rem);
419
+ padding: 0.125rem 0.75rem 0.125rem 0.25rem;
420
+ background-color: var(--header-background-color, rgb(var(--contrast-200)));
421
+ }
422
+
423
+ .header-actions {
424
+ display: flex;
425
+ align-items: center;
426
+ gap: 0.25rem;
427
+ flex-shrink: 0;
428
+ max-width: 50%;
429
+ overflow: hidden;
430
+ box-sizing: border-box;
431
+ max-height: calc(1.25rem + 1rem + 2 * 0.25rem);
432
+ padding: 0.25rem;
433
+ margin: -0.25rem -0.25rem -0.25rem auto;
434
+ color: var(--side-panel-header-text-color);
435
+ text-shadow: none;
436
+ }
437
+
438
+ .has-color {
439
+ --side-panel-header-text-color: rgb(var(--color-white));
440
+ --header-heading-color: var(--side-panel-header-text-color);
441
+ --header-subheading-color: var(--side-panel-header-text-color);
442
+ text-shadow: 0 0 1px rgba(var(--color-black), 0.2), 0 0 0.25rem rgba(var(--color-black), 0.2);
443
+ }
444
+
270
445
  .body {
271
446
  flex-grow: 1;
272
447
  min-height: 0;
@@ -7,12 +7,22 @@ import { createRandomString } from "../../util/random-string";
7
7
  * Escape.
8
8
  */
9
9
  const openPanels = [];
10
+ // The same icons as the navigation buttons in the Lime CRM web client.
11
+ const ACTION_ICONS = {
12
+ newTab: 'up_right_arrow',
13
+ expand: 'expand',
14
+ };
15
+ function getLinkTarget(action) {
16
+ if (action.href && action.type === 'newTab') {
17
+ return '_blank';
18
+ }
19
+ }
10
20
  function stopEvent(event) {
11
21
  event.stopPropagation();
12
22
  }
13
23
  /**
14
24
  * A resizable panel that is displayed on top of the right side of the page.
15
- * The content is placed in the default slot, below a toolbar with a close button.
25
+ * The content is placed in the default slot, below a toolbar and a header.
16
26
  *
17
27
  * The panel is positioned relative to its nearest positioned ancestor. Set
18
28
  * `position: relative` on that element. Use `--side-panel-top-inset`,
@@ -33,9 +43,68 @@ function stopEvent(event) {
33
43
  *
34
44
  * See the examples for how closing and resizing work in detail.
35
45
  *
46
+ * ## Header
47
+ *
48
+ * The header shows the heading, subheading and icon of the last entry in
49
+ * `stack`. When the entry has a `color`, the header gets that background color,
50
+ * and the headings are white with a text shadow. When `stack` is empty, the
51
+ * panel has no header.
52
+ *
53
+ * Put controls for the header, for example a follow button or a menu, in the
54
+ * `header-actions` slot. They are shown at the right end of the header. When
55
+ * `stack` is empty, there is no header, and the slot content is not shown.
56
+ *
57
+ * The slot is for small controls. It is at most half as wide as the header,
58
+ * and at most as tall as the heading and subheading together. Larger content
59
+ * is cut off, so that the header keeps its size.
60
+ *
61
+ * The slot content is not tied to an entry in `stack`. When the last entry
62
+ * changes, the consumer must also change the slot content.
63
+ *
64
+ * The slot content keeps its own colors. Use `--side-panel-header-text-color`
65
+ * to match the text color of the header. It is white when the header has a
66
+ * `color`. It can also be used as the `color` of an `Icon`.
67
+ *
68
+ * ## Actions
69
+ *
70
+ * The toolbar has a close button on the left. Other buttons are set with
71
+ * `actions`, and are shown on the right. The `type` of an action decides its
72
+ * icon. When the user clicks an action, the panel emits `selectAction` with the
73
+ * `id` of the action and the click event. The consumer decides what the action
74
+ * does.
75
+ *
76
+ * An action with an `href` is a link, so the user can open it in a new tab.
77
+ * To handle the navigation in another way, for example with the router of the
78
+ * application, call `preventDefault` on the click event in `selectAction`. Do
79
+ * this only for a plain click with the primary button. Let the browser handle
80
+ * all other clicks, so that Ctrl-click and Cmd-click still open the link in a
81
+ * new tab:
82
+ *
83
+ * ```ts
84
+ * function handleSelectAction(event: CustomEvent<SidePanelActionSelect>) {
85
+ * const click = event.detail.event;
86
+ * const isPlainClick =
87
+ * click.button === 0 &&
88
+ * !click.metaKey &&
89
+ * !click.ctrlKey &&
90
+ * !click.shiftKey &&
91
+ * !click.altKey;
92
+ * if (!isPlainClick) {
93
+ * return;
94
+ * }
95
+ *
96
+ * click.preventDefault();
97
+ * router.navigate(event.detail.id);
98
+ * }
99
+ * ```
100
+ *
36
101
  * @exampleComponent limel-example-side-panel-basic
37
102
  * @exampleComponent limel-example-side-panel-insets
38
103
  * @exampleComponent limel-example-side-panel-escape
104
+ * @exampleComponent limel-example-side-panel-actions
105
+ * @exampleComponent limel-example-side-panel-header-actions
106
+ * @slot - Content of the panel
107
+ * @slot header-actions - Small controls at the right end of the header. Content that is larger than half the header width or the height of the headings is cut off.
39
108
  * @private
40
109
  */
41
110
  export class SidePanel {
@@ -45,6 +114,16 @@ export class SidePanel {
45
114
  * name.
46
115
  */
47
116
  this.closeLabel = 'Close';
117
+ /**
118
+ * The panels in the stack, outermost first. The last entry is the panel
119
+ * that is displayed. Its heading, subheading and icon are shown in the
120
+ * header.
121
+ */
122
+ this.stack = [];
123
+ /**
124
+ * Buttons in the toolbar, to the right of the close button.
125
+ */
126
+ this.actions = [];
48
127
  /**
49
128
  * Label of the resize handle. Used as the accessible name.
50
129
  */
@@ -56,9 +135,28 @@ export class SidePanel {
56
135
  */
57
136
  this.closeOnEscape = true;
58
137
  this.closeButtonId = createRandomString();
59
- this.setBody = (element) => {
60
- this.body = element;
61
- this.stopContentEventsInBody();
138
+ this.actionIdPrefix = createRandomString();
139
+ this.renderAction = (action, index) => {
140
+ const id = `${this.actionIdPrefix}-${index}`;
141
+ const Tag = action.href ? 'a' : 'button';
142
+ return (h(Tag, { class: action.type, id: id, href: action.href, target: getLinkTarget(action), "aria-label": action.label, onClick: this.handleClickAction(action) }, h("limel-icon", { name: ACTION_ICONS[action.type], "aria-hidden": "true" }), h("limel-tooltip", { elementId: id, label: action.label, helperLabel: action.helperLabel })));
143
+ };
144
+ /**
145
+ * Components in the content can emit events with the same names as the
146
+ * panel's events, for example `close` from `limel-dialog` or
147
+ * `limel-collapsible-section`. Events from slotted content pass through the
148
+ * element around the slot before they reach the host, so they are stopped
149
+ * there. The panel's own events are dispatched on the host, so they are not
150
+ * affected.
151
+ *
152
+ * Keep the event names in sync with {@link SidePanel.stopContentEventAtHost}.
153
+ *
154
+ * @param element - the element around a slot
155
+ */
156
+ this.stopContentEvents = (element) => {
157
+ // Adding the same listener again has no effect, so re-renders are safe.
158
+ element === null || element === void 0 ? void 0 : element.addEventListener('close', stopEvent);
159
+ element === null || element === void 0 ? void 0 : element.addEventListener('changeWidth', stopEvent);
62
160
  };
63
161
  // Moving the panel in the DOM also disconnects it. It is then connected
64
162
  // again before the microtask runs. Focus only returns when it was lost with
@@ -130,6 +228,13 @@ export class SidePanel {
130
228
  }
131
229
  this.widthRange = this.measureWidthRange();
132
230
  };
231
+ this.handleClickAction = (action) => (event) => {
232
+ var _a;
233
+ this.selectAction.emit({
234
+ id: (_a = action.id) !== null && _a !== void 0 ? _a : action.type,
235
+ event: event,
236
+ });
237
+ };
133
238
  this.handleClickClose = () => {
134
239
  this.close.emit();
135
240
  };
@@ -152,37 +257,35 @@ export class SidePanel {
152
257
  queueMicrotask(this.restoreFocus);
153
258
  }
154
259
  render() {
155
- return (h(Host, { key: '1c18f40fb45731bdf157d651d4e649446de5230b' }, h("section", { key: 'e8b41bded39732750ae3db9b1daf14dc742e1cb7', ref: this.setPanel, role: "dialog", "aria-label": this.label, tabindex: "-1" }, this.renderHandle(), h("div", { key: '3cba5523e0cbe45fa328daaa40136c063a8c3ac3', class: "content" }, h("div", { key: 'b5cbc1260b4e6f31ac789179ab4b5a0281b9133b', class: "toolbar" }, this.renderCloseButton()), this.renderBody())), h("span", { key: '5fe872f05a6cc49027f84d71a765baa59a53adb9', "data-limit": "min", ref: this.setMinLimit }), h("span", { key: '5042e7437e61ae5308e13b1a30f5df24747e2cdf', "data-limit": "max", ref: this.setMaxLimit })));
260
+ return (h(Host, { key: 'debf8ec5b8e179cbb69c2d87fdd0db7089707dfb' }, h("section", { key: 'd6165d2ff25cad3b9bebe807065b6a83bca24da2', ref: this.setPanel, role: "dialog", "aria-label": this.label, tabindex: "-1" }, this.renderHandle(), h("div", { key: 'b95d937af7fc5799abee8a6042ea2be6087c1b0b', class: "content" }, h("div", { key: 'e258c9475715b00fd1e56a9975db6d0f7e5d19fc', class: "toolbar" }, this.renderCloseButton(), this.renderActions()), this.renderHeader(), this.renderBody())), h("span", { key: '6b7793d7c02f632d630ac4936524104f81a10e23', "data-limit": "min", ref: this.setMinLimit }), h("span", { key: '6196bc2f8549ddd9b8c8bb43bcf221a43f62c98a', "data-limit": "max", ref: this.setMaxLimit })));
156
261
  }
157
262
  // A native button and not `limel-icon-button`, because the hover animation
158
263
  // styles the icon, which is inside the shadow root of `limel-icon-button`.
159
264
  renderCloseButton() {
160
265
  return (h("button", { class: "close", id: this.closeButtonId, "aria-label": this.closeLabel, onClick: this.handleClickClose }, h("limel-icon", { name: "multiply", "aria-hidden": "true" }), h("limel-tooltip", { elementId: this.closeButtonId, label: this.closeLabel })));
161
266
  }
267
+ renderActions() {
268
+ if (this.actions.length === 0) {
269
+ return;
270
+ }
271
+ return (h("div", { class: "actions" }, this.actions.map((action, index) => this.renderAction(action, index))));
272
+ }
273
+ renderHeader() {
274
+ const entry = this.stack.at(-1);
275
+ if (!entry) {
276
+ return;
277
+ }
278
+ return (h("limel-header", { class: { header: true, 'has-color': !!entry.color }, style: { '--header-background-color': entry.color }, icon: entry.icon, heading: entry.heading, subheading: entry.subheading }, h("div", { class: "header-actions", slot: "actions", ref: this.stopContentEvents }, h("slot", { name: "header-actions" }))));
279
+ }
162
280
  renderHandle() {
163
281
  var _a, _b, _c;
164
282
  return (h("limel-resize-handle", { class: "handle", label: this.resizeLabel, value: (_a = this.widthRange) === null || _a === void 0 ? void 0 : _a.now, min: (_b = this.widthRange) === null || _b === void 0 ? void 0 : _b.min, max: (_c = this.widthRange) === null || _c === void 0 ? void 0 : _c.max, onChangeSize: this.handleChangeSize, onEndResize: this.handleEndResize }));
165
283
  }
166
284
  renderBody() {
167
- return (h("div", { class: "body", ref: this.setBody }, h("slot", null)));
168
- }
169
- /**
170
- * Components in the content can emit events with the same names as the
171
- * panel's events, for example `close` from `limel-dialog` or
172
- * `limel-collapsible-section`. Events from slotted content pass through
173
- * `.body` before they reach the host, so they are stopped there. The panel's
174
- * own events are dispatched on the host, so they are not affected.
175
- *
176
- * Keep the event names in sync with {@link SidePanel.stopContentEventAtHost}.
177
- */
178
- stopContentEventsInBody() {
179
- var _a, _b;
180
- // Adding the same listener again has no effect, so re-renders are safe.
181
- (_a = this.body) === null || _a === void 0 ? void 0 : _a.addEventListener('close', stopEvent);
182
- (_b = this.body) === null || _b === void 0 ? void 0 : _b.addEventListener('changeWidth', stopEvent);
285
+ return (h("div", { class: "body", ref: this.stopContentEvents }, h("slot", null)));
183
286
  }
184
287
  /**
185
- * Fallback for {@link SidePanel.stopContentEventsInBody}.
288
+ * Fallback for {@link SidePanel.stopContentEvents}.
186
289
  * Stops `close` and `changeWidth` events that reach the host from any
187
290
  * element other than the host.
188
291
  *
@@ -334,6 +437,56 @@ export class SidePanel {
334
437
  "attribute": "close-label",
335
438
  "defaultValue": "'Close'"
336
439
  },
440
+ "stack": {
441
+ "type": "unknown",
442
+ "mutable": false,
443
+ "complexType": {
444
+ "original": "SidePanelEntry[]",
445
+ "resolved": "SidePanelEntry[]",
446
+ "references": {
447
+ "SidePanelEntry": {
448
+ "location": "import",
449
+ "path": "./side-panel.types",
450
+ "id": "src/components/side-panel/side-panel.types.ts::SidePanelEntry",
451
+ "referenceLocation": "SidePanelEntry"
452
+ }
453
+ }
454
+ },
455
+ "required": false,
456
+ "optional": false,
457
+ "docs": {
458
+ "tags": [],
459
+ "text": "The panels in the stack, outermost first. The last entry is the panel\nthat is displayed. Its heading, subheading and icon are shown in the\nheader."
460
+ },
461
+ "getter": false,
462
+ "setter": false,
463
+ "defaultValue": "[]"
464
+ },
465
+ "actions": {
466
+ "type": "unknown",
467
+ "mutable": false,
468
+ "complexType": {
469
+ "original": "SidePanelAction[]",
470
+ "resolved": "SidePanelAction[]",
471
+ "references": {
472
+ "SidePanelAction": {
473
+ "location": "import",
474
+ "path": "./side-panel.types",
475
+ "id": "src/components/side-panel/side-panel.types.ts::SidePanelAction",
476
+ "referenceLocation": "SidePanelAction"
477
+ }
478
+ }
479
+ },
480
+ "required": false,
481
+ "optional": false,
482
+ "docs": {
483
+ "tags": [],
484
+ "text": "Buttons in the toolbar, to the right of the close button."
485
+ },
486
+ "getter": false,
487
+ "setter": false,
488
+ "defaultValue": "[]"
489
+ },
337
490
  "resizeLabel": {
338
491
  "type": "string",
339
492
  "mutable": false,
@@ -397,6 +550,28 @@ export class SidePanel {
397
550
  "resolved": "void",
398
551
  "references": {}
399
552
  }
553
+ }, {
554
+ "method": "selectAction",
555
+ "name": "selectAction",
556
+ "bubbles": true,
557
+ "cancelable": true,
558
+ "composed": true,
559
+ "docs": {
560
+ "tags": [],
561
+ "text": "Emitted when an action in the toolbar is clicked. The detail has the `id`\nof the action, or its `type` if it has no `id`, and the click event."
562
+ },
563
+ "complexType": {
564
+ "original": "SidePanelActionSelect",
565
+ "resolved": "SidePanelActionSelect",
566
+ "references": {
567
+ "SidePanelActionSelect": {
568
+ "location": "import",
569
+ "path": "./side-panel.types",
570
+ "id": "src/components/side-panel/side-panel.types.ts::SidePanelActionSelect",
571
+ "referenceLocation": "SidePanelActionSelect"
572
+ }
573
+ }
574
+ }
400
575
  }, {
401
576
  "method": "changeWidth",
402
577
  "name": "changeWidth",
@@ -34,6 +34,7 @@ export * from './components/picker/searcher.types';
34
34
  export * from './components/picker/value.types';
35
35
  export * from './components/progress-flow/progress-flow.types';
36
36
  export * from './components/select/option.types';
37
+ export * from './components/side-panel/side-panel.types';
37
38
  export * from './components/spinner/spinner.types';
38
39
  export * from './components/tab-bar/tab.types';
39
40
  export * from './components/tab-panel/tab-panel.types';