@kubex/zinc 1.1.82 → 1.1.84

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.
@@ -3,6 +3,7 @@ meta:
3
3
  title: Cols
4
4
  description: A flexible column layout component that distributes children across multiple columns with configurable layouts, gaps, borders, and padding options.
5
5
  layout: component
6
+ fullWidth: true
6
7
  ---
7
8
 
8
9
  ```html:preview
@@ -74,6 +75,48 @@ The layout pattern repeats for additional children. With "2,1", items cycle thro
74
75
  </zn-cols>
75
76
  ```
76
77
 
78
+ ### Stacking Order
79
+
80
+ Use `stack-at` to name the container width the columns collapse to a single column below, as one of
81
+ the container sizes (`sm`, `smp`, `ph`, `md`, `lg`, `hd`, `3k`, `4k`). Children can then declare a
82
+ `stack-order` to say where they belong in that single column, which lets a sidebar sit second in the
83
+ markup but move to the top once stacked.
84
+
85
+ `stack-order` takes `first`/`high` (before unordered children), `last`/`low` (after them), or any
86
+ integer for finer control. It has no effect while the columns are side by side, so the reading order
87
+ of your markup is what wide screens get.
88
+
89
+ ```html:preview
90
+ <zn-cols layout="2,1" stack-at="md">
91
+ <zn-panel>Main content — first in the markup, second when stacked</zn-panel>
92
+ <zn-panel stack-order="first">Sidebar — second in the markup, first when stacked</zn-panel>
93
+ </zn-cols>
94
+ ```
95
+
96
+ ### Splitting a Column by Priority
97
+
98
+ Add `stack-split` to a column to break it apart when stacked. Its children stay together as one
99
+ column on wide screens, then each becomes a column in its own right and takes its own `stack-order`,
100
+ so high priority content can move above the main column while low priority content drops below it.
101
+
102
+ ```html:preview
103
+ <zn-cols layout="2,1" stack-at="md">
104
+ <zn-panel>Databases — the main column</zn-panel>
105
+ <div stack-split>
106
+ <zn-panel stack-order="high">Usage — above the main column when stacked</zn-panel>
107
+ <zn-panel stack-order="low">Tutorials — below the main column when stacked</zn-panel>
108
+ </div>
109
+ </zn-cols>
110
+ ```
111
+
112
+ Keep `stack-split` on a plain wrapper element rather than a component: `zn-cols` lays it out as a
113
+ column (`display: flex` in a column, with the standard gap) and then removes its box entirely while
114
+ stacked, so any styling of its own would disappear along with it.
115
+
116
+ `stack-at` defaults to `lg` when any child declares `stack-order` or `stack-split`. Set it explicitly
117
+ at or above the width where your columns would otherwise wrap on their own, so that reordering is
118
+ already in effect by the time the layout breaks.
119
+
77
120
  ### With Borders
78
121
 
79
122
  Use the `border` attribute to add borders around columns.
@@ -294,12 +294,14 @@ no dropdown rendered.
294
294
  ## Collapsing the controls column
295
295
 
296
296
  Set `controls-collapsed` to hide the controls column, or click the chevron
297
- toggle that sits on the seam between the columns. Collapsing is purely a
297
+ toggle that straddles the seam between the columns the same edge chevron
298
+ the flow and page builders use for their side panels. Collapsing is purely a
298
299
  layout change — it never affects harvested values or pushes a new theme to
299
- the preview. Below the 768px stacked breakpoint the toggle is hidden, since
300
- there's no side-by-side seam to tuck into the editor also un-collapses
301
- itself if it's already showing `controls-collapsed` when the layout narrows
302
- that far, so the controls are never stuck unreachable.
300
+ the preview. Below the 768px stacked breakpoint the columns stack vertically
301
+ and the chevron attaches to the horizontal seam above the preview instead;
302
+ crossing into that breakpoint also auto-collapses the controls (once —
303
+ re-expanding while narrow is respected), and the toggle stays available to
304
+ bring them back.
303
305
 
304
306
  ## Standalone panel
305
307
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubex/zinc",
3
- "version": "1.1.82",
3
+ "version": "1.1.84",
4
4
  "description": "A collection of web components for building web applications based off of @shoelace-style/Shoelace",
5
5
  "keywords": [
6
6
  "web components",
@@ -5,6 +5,18 @@ import ZincElement from '../../internal/zinc-element';
5
5
 
6
6
  import styles from './cols.scss';
7
7
 
8
+ /**
9
+ * Priority keywords accepted by the `stack-order` attribute on children.
10
+ */
11
+ const STACK_ORDER_KEYWORDS: Record<string, number> = {
12
+ first: -1,
13
+ high: -1,
14
+ last: 1,
15
+ low: 1,
16
+ };
17
+
18
+ const DEFAULT_STACK_AT = 'lg';
19
+
8
20
  /**
9
21
  * @summary Short summary of the component's intended use.
10
22
  * @documentation https://zinc.style/components/columns
@@ -27,6 +39,13 @@ export default class ZnCols extends ZincElement {
27
39
 
28
40
  @property({reflect: true, attribute: 'layout'}) layout: string = '';
29
41
 
42
+ /**
43
+ * Container width the columns collapse to a single column below, as one of the named zinc
44
+ * container sizes (sm, smp, ph, md, lg, hd, 3k, 4k). Defaults to `lg` when a child declares
45
+ * `stack-order` or `stack-split`, otherwise the columns never explicitly stack.
46
+ */
47
+ @property({attribute: 'stack-at', reflect: true}) stackAt: string = '';
48
+
30
49
  @property({attribute: 'mc', type: Number, reflect: true}) maxColumns: number = 0;
31
50
 
32
51
  @property({attribute: 'no-gap', type: Boolean}) noGap: boolean = false;
@@ -41,6 +60,52 @@ export default class ZnCols extends ZincElement {
41
60
 
42
61
  @property({attribute: 'pad-y', type: Boolean}) padY: boolean;
43
62
 
63
+ // Column classes and stack ordering are written onto our children, so a re-render is needed
64
+ // whenever they change. Registered on the host and on each [stack-split] column, rather than
65
+ // the whole subtree, to avoid reacting to unrelated content updates deeper in a column.
66
+ private readonly childObserver: MutationObserver = new MutationObserver(() => this.requestUpdate());
67
+
68
+ connectedCallback() {
69
+ super.connectedCallback();
70
+ this.childObserver.observe(this, {childList: true});
71
+ }
72
+
73
+ disconnectedCallback() {
74
+ super.disconnectedCallback();
75
+ this.childObserver.disconnect();
76
+ }
77
+
78
+ /**
79
+ * Reads the `stack-order` attribute of an element, returning null when it does not declare one.
80
+ */
81
+ private stackOrder(element: Element): number | null {
82
+ const raw = element.getAttribute('stack-order');
83
+ if (raw === null) return null;
84
+
85
+ const keyword = raw.trim().toLowerCase();
86
+ if (keyword in STACK_ORDER_KEYWORDS) return STACK_ORDER_KEYWORDS[keyword];
87
+
88
+ const order = parseInt(keyword, 10);
89
+ return isNaN(order) ? null : order;
90
+ }
91
+
92
+ /**
93
+ * Applies the declared stack order to an element. `--zn-stacked` is 0 until the container
94
+ * query in our stylesheet flips it to 1, so ordering only kicks in once stacked.
95
+ */
96
+ private applyStackOrder(element: HTMLElement, promoted: boolean): number | null {
97
+ const order = this.stackOrder(element);
98
+ element.style.order = order === null ? '' : `calc(var(--zn-stacked, 0) * ${order})`;
99
+
100
+ // Children of a [stack-split] column become columns themselves when stacked, at which point
101
+ // they need a basis to fill the row with. --zn-stack-basis is only set while stacked.
102
+ if (promoted) {
103
+ element.style.flexBasis = 'var(--zn-stack-basis, auto)';
104
+ }
105
+
106
+ return order;
107
+ }
108
+
44
109
  render() {
45
110
  const layout: number[] = this.layout.split(/[\s,]+/).map((a) => parseInt(a)).filter((item) => !!item);
46
111
 
@@ -58,6 +123,8 @@ export default class ZnCols extends ZincElement {
58
123
  const colsPerRow = layout.length;
59
124
  const lastRowStart = children.length - (children.length % colsPerRow || colsPerRow);
60
125
 
126
+ let stacks = false;
127
+
61
128
  children.forEach((element, index) => {
62
129
  const classes = element.className.split(' ').filter((c) => !c.startsWith(prefix));
63
130
  element.className = classes.join(' ');
@@ -71,8 +138,20 @@ export default class ZnCols extends ZincElement {
71
138
  } else {
72
139
  element.style.overflow = '';
73
140
  }
141
+
142
+ stacks = this.applyStackOrder(element, false) !== null || stacks;
143
+
144
+ if(element.hasAttribute('stack-split')) {
145
+ stacks = true;
146
+ this.childObserver.observe(element, {childList: true});
147
+ Array.from(element.children).forEach((child) => this.applyStackOrder(child as HTMLElement, true));
148
+ }
74
149
  });
75
150
 
151
+ if(stacks && !this.stackAt) {
152
+ this.stackAt = DEFAULT_STACK_AT;
153
+ }
154
+
76
155
  return html`
77
156
  <div part="base" class="${classMap({
78
157
  'cols': true,
@@ -87,6 +87,14 @@
87
87
  border-bottom: 1px solid rgb(var(--zn-border-color)) !important;
88
88
  }
89
89
 
90
+ // Children marked with [stack-split] act as a single column while the cols are
91
+ // side by side, and dissolve into individual columns once stacked (see below).
92
+ ::slotted([stack-split]) {
93
+ display: flex;
94
+ flex-direction: column;
95
+ gap: var(--zn-gap);
96
+ }
97
+
90
98
  &--pad {
91
99
  padding: var(--zn-spacing-2x-small);
92
100
  }
@@ -99,3 +107,40 @@
99
107
  padding-block: var(--zn-spacing-2x-small);
100
108
  }
101
109
  }
110
+
111
+ //
112
+ // Responsive stacking
113
+ //
114
+ // `stack-at` names the container width the cols collapse to a single column below.
115
+ // While stacked, `--zn-stacked` flips to 1 so the per-child `order: calc(var(--zn-stacked) * n)`
116
+ // written by the component only takes effect in the stacked state, and `--zn-stack-basis`
117
+ // gives promoted [stack-split] children something to size against.
118
+ //
119
+ // `:host()` is only in the selector to out-specify the `.zn-col-#{$i}` min-width rules above.
120
+ //
121
+ $stack-sizes: (sm, smp, ph, md, lg, hd, '3k', '4k');
122
+
123
+ @each $size in $stack-sizes {
124
+ :host([stack-at="#{$size}"]) {
125
+ @include wc.container-query(null, $size) {
126
+ .cols ::slotted(*) {
127
+ --zn-stacked: 1;
128
+ --zn-stack-basis: 100%;
129
+
130
+ flex-basis: 100%;
131
+ min-width: 100%;
132
+ max-width: 100%;
133
+ }
134
+
135
+ // Hand this column's children over to the cols so each can be ordered individually
136
+ .cols ::slotted([stack-split]) {
137
+ display: contents;
138
+ }
139
+
140
+ // Vertical dividers make no sense once the columns are on top of each other
141
+ .cols--divide ::slotted(*:not(:last-child)):after {
142
+ display: none;
143
+ }
144
+ }
145
+ }
146
+ }
@@ -1,10 +1,138 @@
1
1
  import '../../../dist/zn.min.js';
2
- import { expect, fixture, html } from '@open-wc/testing';
2
+ import {aTimeout, expect, fixture} from '@open-wc/testing';
3
+ import type {LitElement} from 'lit';
3
4
 
4
- describe('<zn-columns>', () => {
5
+ /** Reads the flex order the browser actually resolved for a light DOM child */
6
+ const orderOf = (el: Element | null) => getComputedStyle(el!).order;
7
+
8
+ const cols = async (markup: string) => {
9
+ const el = await fixture<LitElement>(markup);
10
+ await el.updateComplete;
11
+ return el;
12
+ };
13
+
14
+ describe('<zn-cols>', () => {
5
15
  it('should render a component', async () => {
6
- const el = await fixture(html` <zn-columns></zn-columns> `);
16
+ const el = await fixture('<zn-cols></zn-cols>');
7
17
 
8
18
  expect(el).to.exist;
9
19
  });
20
+
21
+ it('should assign column classes from the layout pattern', async () => {
22
+ const el = await cols(`
23
+ <zn-cols layout="2,1">
24
+ <div id="main"></div>
25
+ <div id="side"></div>
26
+ <div id="wrapped"></div>
27
+ </zn-cols>`);
28
+
29
+ expect(el.querySelector('#main')).to.have.class('zn-col-2');
30
+ expect(el.querySelector('#side')).to.have.class('zn-col-1');
31
+ expect(el.querySelector('#wrapped')).to.have.class('zn-col-2');
32
+ });
33
+
34
+ describe('stacking', () => {
35
+ it('should not stack unless asked to', async () => {
36
+ const el = await cols(`
37
+ <zn-cols layout="2,1">
38
+ <div></div>
39
+ <div></div>
40
+ </zn-cols>`);
41
+
42
+ expect(el.getAttribute('stack-at')).to.equal('');
43
+ });
44
+
45
+ it('should default stack-at when a child declares a stack order', async () => {
46
+ const el = await cols(`
47
+ <zn-cols layout="2,1">
48
+ <div></div>
49
+ <div stack-order="first"></div>
50
+ </zn-cols>`);
51
+ await el.updateComplete;
52
+
53
+ expect(el.getAttribute('stack-at')).to.equal('lg');
54
+ });
55
+
56
+ it('should keep an explicit stack-at', async () => {
57
+ const el = await cols(`
58
+ <zn-cols layout="2,1" stack-at="md">
59
+ <div></div>
60
+ <div stack-order="first"></div>
61
+ </zn-cols>`);
62
+ await el.updateComplete;
63
+
64
+ expect(el.getAttribute('stack-at')).to.equal('md');
65
+ });
66
+
67
+ it('should only apply the stack order once stacked', async () => {
68
+ const el = await cols(`
69
+ <zn-cols layout="2,1" stack-at="md" style="width: 900px">
70
+ <div id="main"></div>
71
+ <div id="side" stack-order="first"></div>
72
+ </zn-cols>`);
73
+
74
+ expect(orderOf(el.querySelector('#side'))).to.equal('0');
75
+
76
+ el.style.width = '500px';
77
+ await aTimeout(50);
78
+
79
+ expect(orderOf(el.querySelector('#side'))).to.equal('-1');
80
+ expect(orderOf(el.querySelector('#main'))).to.equal('0');
81
+ });
82
+
83
+ it('should accept keywords and integers as a stack order', async () => {
84
+ const el = await cols(`
85
+ <zn-cols layout="2,1" stack-at="md" style="width: 500px">
86
+ <div id="a" stack-order="high"></div>
87
+ <div id="b" stack-order="low"></div>
88
+ <div id="c" stack-order="3"></div>
89
+ <div id="d" stack-order="nonsense"></div>
90
+ </zn-cols>`);
91
+
92
+ expect(orderOf(el.querySelector('#a'))).to.equal('-1');
93
+ expect(orderOf(el.querySelector('#b'))).to.equal('1');
94
+ expect(orderOf(el.querySelector('#c'))).to.equal('3');
95
+ expect(orderOf(el.querySelector('#d'))).to.equal('0');
96
+ });
97
+
98
+ it('should split a column into individually ordered children once stacked', async () => {
99
+ const el = await cols(`
100
+ <zn-cols layout="2,1" stack-at="md" style="width: 900px">
101
+ <div id="main" style="height: 100px"></div>
102
+ <div id="side" stack-split>
103
+ <div id="usage" stack-order="high" style="height: 20px"></div>
104
+ <div id="tutorials" stack-order="low" style="height: 20px"></div>
105
+ </div>
106
+ </zn-cols>`);
107
+
108
+ // Side by side, the split column is a single column holding both of its children
109
+ expect(getComputedStyle(el.querySelector('#side')!).display).to.equal('flex');
110
+ expect(orderOf(el.querySelector('#usage'))).to.equal('0');
111
+
112
+ el.style.width = '500px';
113
+ await aTimeout(50);
114
+
115
+ // Stacked, the column dissolves so each of its children becomes a column of its own
116
+ expect(getComputedStyle(el.querySelector('#side')!).display).to.equal('contents');
117
+ expect(orderOf(el.querySelector('#usage'))).to.equal('-1');
118
+ expect(orderOf(el.querySelector('#tutorials'))).to.equal('1');
119
+
120
+ const top = (selector: string) => el.querySelector(selector)!.getBoundingClientRect().top;
121
+ expect(top('#usage')).to.be.lessThan(top('#main'));
122
+ expect(top('#tutorials')).to.be.greaterThan(top('#main'));
123
+ });
124
+
125
+ it('should order children added after the initial render', async () => {
126
+ const el = await cols(`
127
+ <zn-cols layout="2,1" stack-at="md" style="width: 500px">
128
+ <div id="main"></div>
129
+ <div id="side" stack-split></div>
130
+ </zn-cols>`);
131
+
132
+ el.querySelector('#side')!.innerHTML = '<div id="usage" stack-order="high"></div>';
133
+ await aTimeout(50);
134
+
135
+ expect(orderOf(el.querySelector('#usage'))).to.equal('-1');
136
+ });
137
+ });
10
138
  });
@@ -1,6 +1,7 @@
1
1
  @use "../../wc";
2
2
 
3
3
  :host {
4
+ @include wc.scrollbars; // styles scrollbars of all descendant scroll areas (controls column), as the other builders do
4
5
  display: block;
5
6
  // Matches page-builder's --palette-col.
6
7
  --zn-theme-editor-controls-width: 343px;
@@ -65,6 +66,10 @@
65
66
  }
66
67
 
67
68
  .editor__controls {
69
+ // Colour the tab strip's active tab to match the controls panel surface,
70
+ // rather than the navbar's default body colour (flow-builder pattern).
71
+ --zn-active-nav-background: var(--zn-panel);
72
+
68
73
  position: relative;
69
74
  display: flex;
70
75
  flex-direction: column;
@@ -192,39 +197,41 @@
192
197
  flex-direction: column;
193
198
  }
194
199
 
195
- // A pull tab flush against the controls/main seam, rather than a dot floating
196
- // over it, positioned against .editor__main (not .editor__preview) so it stays
197
- // centred on the full-height seam rather than just the preview's portion of it.
198
- // Collapsed there is no seam to attach to, so it becomes a small pill.
200
+ // Chevron straddling the controls/main seam tuck the controls away / bring
201
+ // them back (flow-builder pattern). Positioned against .editor__main (not
202
+ // .editor__preview) so it stays centred on the full-height seam rather than
203
+ // just the preview's portion of it.
199
204
  .panel-toggle {
200
205
  position: absolute;
201
206
  top: 50%;
202
- left: 0;
203
207
  z-index: 6;
204
208
  transform: translateY(-50%);
205
209
  display: flex;
206
210
  align-items: center;
207
211
  justify-content: center;
208
- width: 18px;
209
- height: 44px;
212
+ width: 24px;
213
+ height: 24px;
210
214
  padding: 0;
211
- border: 1px solid rgb(var(--zn-border-color));
212
- border-left: 0;
213
- border-radius: 0 6px 6px 0;
215
+ border-radius: 50%;
214
216
  color: rgb(var(--zn-color-muted-text));
215
217
  background: rgb(var(--zn-panel));
218
+ border: 1px solid rgb(var(--zn-border-color));
219
+ box-shadow: 0 1px 3px rgba(var(--zn-shadow), 0.4);
216
220
  cursor: pointer;
217
- transition: left 0.2s ease, color 0.12s ease, border-color 0.12s ease;
221
+ transition: left 0.2s ease, right 0.2s ease, color 0.12s ease, border-color 0.12s ease;
218
222
 
219
223
  &:hover {
220
224
  color: rgb(var(--zn-text));
221
- background: rgba(var(--zn-border-color), 0.4);
225
+ border-color: rgb(var(--zn-color-border-active));
222
226
  }
223
227
 
224
- &--tucked {
225
- left: var(--zn-spacing-small);
226
- border-left: 1px solid rgb(var(--zn-border-color));
227
- border-radius: 6px;
228
+ &--left {
229
+ left: -12px;
230
+
231
+ // With the panel tucked away, sit fully inside the preview column.
232
+ &.panel-toggle--tucked {
233
+ left: 8px;
234
+ }
228
235
  }
229
236
  }
230
237
 
@@ -336,22 +343,19 @@
336
343
  padding-top: var(--zn-spacing-medium);
337
344
  }
338
345
 
339
- // Stacked: the seam is horizontal, so the tab attaches to the top edge instead.
346
+ // Stacked: the seam is horizontal, so the chevron straddles the top edge instead.
340
347
  .panel-toggle {
341
- top: 0;
342
- left: 50%;
343
- width: 44px;
344
- height: 18px;
345
- border: 1px solid rgb(var(--zn-border-color));
346
- border-top: 0;
347
- border-radius: 0 0 6px 6px;
348
+ top: -12px;
348
349
  transform: translateX(-50%);
349
- }
350
+ transition: top 0.2s ease, color 0.12s ease, border-color 0.12s ease;
350
351
 
351
- .panel-toggle--tucked {
352
- top: var(--zn-spacing-small);
353
- left: 50%;
354
- border-top: 1px solid rgb(var(--zn-border-color));
355
- border-radius: 6px;
352
+ &--left {
353
+ left: 50%;
354
+
355
+ &.panel-toggle--tucked {
356
+ top: 8px;
357
+ left: 50%;
358
+ }
359
+ }
356
360
  }
357
361
  }