@keenmate/pure-css 1.0.6 → 1.1.1

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.
@@ -1,459 +1,447 @@
1
- /* ========================================
2
- Sidebar Component
3
- Sidebar navigation with nested submenus
4
- ======================================== */
5
- @use 'variables/index' as *;
6
- @use 'resize-handle' as *;
7
-
8
- // Local CSS variables for sidebar (runtime state, modified by JS for resize)
9
- :root {
10
- --pc-local-sidebar-width: #{$sidebar-width};
11
- --pc-local-sidebar-min-width: 18rem;
12
- --pc-local-sidebar-max-width: 50rem;
13
- }
14
-
15
- // Sidebar layout element - width in :where() for low specificity (utility classes can override)
16
- :where(.pc-layout__sidebar) {
17
- width: var(--pc-local-sidebar-width);
18
- }
19
-
20
- .pc-layout__sidebar {
21
- background-color: var(--pc-sidebar-bg, var(--base-page-bg));
22
- border-inline-end: $border-width-base solid var(--pc-border-color); // RTL: flips to border-left
23
- flex-shrink: 0;
24
- position: relative;
25
- }
26
-
27
- // Resize handle on the sidebar edge. Shows the shared resize knob (see
28
- // _resize-handle.scss) so it's the same control as the splitter gutter — slim on
29
- // desktop, chunkier on small viewports. The knob is sticky at mid-viewport so it
30
- // stays on screen on long, page-height sidebars.
31
- .pc-sidebar-resize {
32
- position: absolute;
33
- top: 0;
34
- inset-inline-end: 0; // RTL: flips to left
35
- width: 6px;
36
- height: 100%;
37
- cursor: ew-resize;
38
- z-index: 100;
39
- // Own the touch gesture. Without this the browser's touch-action heuristic can
40
- // interpret a drag as a scroll/pan and CANCEL the touch sequence after
41
- // touchstart — so touchmove never reaches the resize handler and the drag
42
- // silently dies. Harmless for mouse (mousedown path is unaffected).
43
- touch-action: none;
44
- // Flex so the sticky knob can pin to the inline-end edge and ride the viewport.
45
- display: flex;
46
- justify-content: flex-end;
47
- align-items: flex-start;
48
-
49
- // Shared grab-knob. `position: sticky; top: 50vh` keeps it at the middle of the
50
- // VISIBLE viewport, not the middle of the sidebar's full length — on a long
51
- // page the sidebar stretches to the whole scroll height, so a plain top:50%
52
- // knob ends up far off-screen. flex-end pins it to the inline-end edge;
53
- // translateX(50%) straddles it onto the sidebar's border line; translateY(-50%)
54
- // centres it on the 50vh sticky point.
55
- &::before {
56
- @include pa-resize-knob($axis: block);
57
- position: sticky;
58
- top: 50vh;
59
- transform: translate(50%, -50%);
60
- }
61
-
62
- &:hover::before,
63
- &--active::before {
64
- @include pa-resize-knob-active;
65
- }
66
- }
67
-
68
- // Touch: widen only the (invisible) grab area to a finger-sized target. The knob
69
- // LOOK is viewport-driven (handled by the shared mixin), so it's unchanged here.
70
- @media (pointer: coarse) {
71
- .pc-sidebar-resize {
72
- width: 24px;
73
- }
74
- }
75
-
76
- // Sticky LAYOUT mode: the whole page doesn't scroll (body overflow:hidden) and
77
- // the sidebar is already ~viewport height, where position:sticky never engages.
78
- // Absolute-centre the knob instead — 50% of a viewport-height sidebar IS the
79
- // viewport centre.
80
- body.pc-layout--sticky .pc-sidebar-resize::before {
81
- position: absolute;
82
- top: 50%;
83
- inset-inline-end: 0;
84
- }
85
-
86
- // Disable text selection during resize
87
- body.pc-sidebar-resizing {
88
- cursor: ew-resize !important;
89
- user-select: none;
90
-
91
- * {
92
- cursor: ew-resize !important;
93
- }
94
- }
95
-
96
- // Sticky mode - sidebar scrolls
97
- body.pc-layout--sticky .pc-layout__sidebar {
98
- overflow-y: auto;
99
- overflow-y: overlay; // Chromium: scrollbar overlays content, doesn't reduce width
100
- overflow-x: visible; // Allow tooltips to escape (right in LTR, left in RTL)
101
- }
102
-
103
- // Sidebar component (separate BEM block)
104
- .pc-sidebar {
105
- &__nav {
106
- padding: $spacing-base 0;
107
-
108
- ul {
109
- list-style: none;
110
- margin: 0;
111
- padding: 0;
112
- }
113
- }
114
-
115
- &__item {
116
- // Base styles for all sidebar items (regular links and submenu toggles)
117
- }
118
-
119
- // Flat section heading — a non-interactive group label ("PROJECT",
120
- // "GUIDES", "LIVE DEMOS"). Use when the nav is grouped but the groups are
121
- // NOT collapsible; for collapsible groups use __toggle + __submenu instead.
122
- // Rendered as an <li class="pc-sidebar__section"> inside __nav's <ul>, so it
123
- // sits in the list flow without a bullet (the <ul> already resets list-style).
124
- &__section {
125
- padding: $spacing-md $spacing-base $spacing-xs; // 16px inset aligns with __link text
126
- font-size: $font-size-xs;
127
- font-weight: $font-weight-semibold;
128
- text-transform: uppercase;
129
- letter-spacing: 0.05em;
130
- color: var(--pc-sidebar-text-secondary, var(--base-text-color-2));
131
- user-select: none;
132
-
133
- // First heading shouldn't push the whole nav down
134
- &:first-child {
135
- padding-top: $spacing-xs;
136
- }
137
- }
138
-
139
- // Thin horizontal rule separating groups of items — e.g. between nav items
140
- // folded in from the navbar (fit.js nav-collapse, sidebar sink) and the
141
- // sidebar's own links. Rendered as an <li class="pc-sidebar__divider"> so it
142
- // sits in the list flow; carries no text.
143
- &__divider {
144
- height: 0;
145
- margin: $spacing-xs $spacing-base;
146
- padding: 0;
147
- border: 0;
148
- border-top: $border-width-base solid var(--pc-border-color);
149
- list-style: none;
150
- }
151
-
152
- &__link {
153
- display: flex;
154
- align-items: center;
155
- gap: $sidebar-item-gap;
156
- padding: $sidebar-padding;
157
- color: var(--pc-sidebar-text-secondary, var(--base-text-color-2));
158
- text-decoration: none;
159
- font-weight: $font-weight-medium;
160
- border-inline-end: $border-width-thick solid transparent; // RTL: flips to border-left
161
-
162
- body.loaded & {
163
- transition: all $transition-fast $easing-snappy;
164
- }
165
-
166
- &:hover {
167
- color: var(--pc-sidebar-text, var(--base-text-color-1));
168
- background-color: var(--pc-accent-light);
169
- }
170
-
171
- &--active {
172
- color: var(--pc-accent);
173
- background-color: var(--pc-accent-hover);
174
- border-inline-end-color: var(--pc-accent); // RTL: flips to border-left-color
175
- font-weight: $font-weight-semibold;
176
- }
177
- }
178
-
179
- // Search affordance at the top of the sidebar — a TRIGGER (a consumer wires it
180
- // to open a command palette / their own search), rendered as a framed
181
- // faux-input row so it reads as "search" rather than another nav link:
182
- // magnifier icon + muted label, inset from the sidebar edges. In icon-collapse
183
- // mode it sheds the frame and centres to just the icon, exactly like the links
184
- // below (its .pc-sidebar__label is hidden by the shared icon-collapse rule).
185
- &__search {
186
- display: flex;
187
- align-items: center;
188
- gap: $sidebar-item-gap;
189
- width: calc(100% - #{$spacing-sm} * 2);
190
- margin: $spacing-sm;
191
- // Sized on the md input height (35px) to match the navbar search boxes,
192
- // rather than the icon + $sidebar-padding metric of a nav row (40px). Trade-off:
193
- // it sits ~5px shorter than the links below it. Block padding is dropped (fixed
194
- // height + align-items:center owns the vertical).
195
- // Inline padding: right = $spacing-base (the nav-row inset); left = $spacing-sm
196
- // so that margin ($spacing-sm) + padding-left ($spacing-sm) = $spacing-base —
197
- // landing the magnifier exactly where the nav-row icons below it sit, instead
198
- // of one box-margin too far in.
199
- height: #{$base-input-size-md-height}rem;
200
- padding: 0 $spacing-base 0 $spacing-sm;
201
- color: var(--pc-text-color-2);
202
- background-color: var(--base-input-bg);
203
- border: $border-width-base solid var(--pc-border-color);
204
- border-radius: var(--pc-border-radius);
205
- text-align: start; // RTL: flips to right
206
- font-weight: $font-weight-medium;
207
- cursor: pointer;
208
-
209
- // Honor the standard HTML hidden attribute (display:flex above beats the UA
210
- // `[hidden]{display:none}` rule otherwise).
211
- &[hidden] {
212
- display: none;
213
- }
214
-
215
- body.loaded & {
216
- transition: all $transition-fast $easing-snappy;
217
- }
218
-
219
- &:hover {
220
- // $accent-color (foundation) — was $input-focus-border-color, an exact
221
- // alias; inlined so the shell no longer depends on the component layer
222
- // (variables/_components.scss now lives in pure-admin-core).
223
- border-color: $accent-color;
224
- // This framed box sits on the INPUT surface (--base-input-bg above), NOT
225
- // the sidebar surface — so emphasise with the input text colour, not
226
- // --pc-sidebar-text. In themes with a mode-independent sidebar (e.g.
227
- // express: always-black sidebar → --pc-sidebar-text is white) the old
228
- // sidebar-text hover went white-on-white on the light input bg.
229
- color: var(--base-input-color);
230
- }
231
-
232
- // Collapsed rail ONLY (icon-collapse mode AND actually collapsed, i.e.
233
- // body.sidebar-hidden): drop the frame, icon-only, matching link padding so
234
- // the icon lands in the same column as every other sidebar icon. Must be
235
- // gated by .sidebar-hidden — .pc-layout__sidebar--icon-collapse is the MODE
236
- // class and stays on the element while the rail is expanded, so an unscoped
237
- // rule would strip the search frame in the expanded state too.
238
- .sidebar-hidden .pc-layout__sidebar--icon-collapse & {
239
- width: auto;
240
- margin: 0;
241
- padding: $sidebar-padding;
242
- background: transparent;
243
- border-color: transparent;
244
- border-radius: 0;
245
- }
246
-
247
- // Type-and-go variant: the framed row hosts a <form> whose native GET submit
248
- // redirects to a results page on Enter — instead of a button that opens the
249
- // palette. A single text field submits on Enter with no submit button; the
250
- // magnifier is a type="submit" so that in icon-collapse mode (field hidden)
251
- // clicking it still submits — an empty query lands on the bare results page.
252
- // Keeps the base $sidebar-padding frame untouched so the outer box is
253
- // identical to the trigger button; the submit icon mirrors .pc-sidebar__icon
254
- // (fixed size, line-height 1) so it drives the row height exactly like a link,
255
- // and the borderless field just fills the rest. (Don't feed $sidebar-padding —
256
- // a two-value 0.5rem/1rem token — to padding-block: it maps to top/bottom and
257
- // inflates the box.)
258
- &--input {
259
- cursor: text;
260
-
261
- .pc-sidebar__search-icon {
262
- width: $sidebar-icon-size;
263
- min-width: $sidebar-icon-size;
264
- height: $sidebar-icon-size;
265
- flex-shrink: 0;
266
- padding: 0;
267
- border: none;
268
- cursor: pointer;
269
- color: inherit;
270
- // Masked magnifier painted in currentColor — was a 🔍 text emoji, which
271
- // rendered inconsistently across platforms/fonts (the same reason the
272
- // sidebar chevron is masked above). Reads the shared --base-icon-search
273
- // so it matches pure-admin's .pa-icon--search and the navbar search;
274
- // #{$base-icon-search} is the standalone literal fallback. font-size:0
275
- // collapses any legacy 🔍 still in markup so it never double-renders.
276
- font-size: 0;
277
- background-color: currentColor;
278
- -webkit-mask: var(--base-icon-search, #{$base-icon-search}) center / #{$font-size-base} no-repeat;
279
- mask: var(--base-icon-search, #{$base-icon-search}) center / #{$font-size-base} no-repeat;
280
- }
281
-
282
- .pc-sidebar__search-field {
283
- flex: 1;
284
- min-width: 0;
285
- border: none;
286
- outline: none;
287
- background: transparent;
288
- // The field shows through to the framed box's --base-input-bg surface,
289
- // so its text must use the input text colour (--base-input-color), not
290
- // the sidebar text colour. --pc-sidebar-text is tuned for the sidebar
291
- // surface, which can be mode-independent (express: always-black sidebar
292
- // → white sidebar text) and then renders white-on-white on a light
293
- // input bg. --base-input-color is emitted per-mode alongside
294
- // --base-input-bg, so the pair is always legible.
295
- color: var(--base-input-color);
296
- font: inherit;
297
- padding: 0;
298
-
299
- &::placeholder {
300
- color: var(--pc-text-color-2);
301
- opacity: $opacity-muted;
302
- }
303
-
304
- &::-webkit-search-decoration,
305
- &::-webkit-search-cancel-button {
306
- -webkit-appearance: none;
307
- }
308
- }
309
- }
310
-
311
- // Collapsed rail ONLY: the input variant sheds its field and centres on the
312
- // submit icon; clicking it submits with an empty query (the bare results
313
- // page). Gated by .sidebar-hidden so the field reappears when the icon-collapse
314
- // rail is expanded (see frame rule above for why the mode class isn't enough).
315
- .sidebar-hidden .pc-layout__sidebar--icon-collapse &--input {
316
- justify-content: center;
317
-
318
- .pc-sidebar__search-field {
319
- display: none;
320
- }
321
- }
322
- }
323
-
324
- &__toggle {
325
- // For submenu toggle buttons (non-link items)
326
- display: flex;
327
- align-items: center;
328
- gap: $sidebar-item-gap;
329
- padding: $sidebar-padding;
330
- color: var(--pc-sidebar-text-secondary, var(--base-text-color-2));
331
- background: none;
332
- border: none;
333
- width: 100%;
334
- text-align: start; // RTL: flips to right
335
- font-weight: $font-weight-medium;
336
- cursor: pointer;
337
-
338
- body.loaded & {
339
- transition: all $transition-fast $easing-snappy;
340
- }
341
-
342
- &:hover {
343
- color: var(--pc-sidebar-text, var(--base-text-color-1));
344
- background-color: var(--pc-accent-light);
345
- }
346
-
347
- &--active {
348
- color: var(--pc-accent);
349
- background-color: var(--pc-accent-hover);
350
- }
351
- }
352
-
353
- // Icon and label styles
354
- &__icon {
355
- width: $sidebar-icon-size;
356
- min-width: $sidebar-icon-size;
357
- height: $sidebar-icon-size;
358
- display: flex;
359
- align-items: center;
360
- justify-content: center;
361
- flex-shrink: 0;
362
- font-size: $font-size-base;
363
- text-align: center;
364
- line-height: 1; // Prevent emoji baseline shift
365
- }
366
-
367
- &__label {
368
- flex: 1;
369
- overflow: hidden;
370
- text-overflow: ellipsis;
371
- white-space: nowrap;
372
-
373
- body.loaded & {
374
- transition: opacity $transition-normal $easing-snappy, width $transition-normal $easing-snappy;
375
- }
376
- }
377
-
378
- // Submenu styles - Level 2
379
- &__submenu {
380
- list-style: none;
381
- margin: 0;
382
- padding: 0;
383
- display: none;
384
- background-color: var(--pc-sidebar-submenu-bg, var(--base-subtle-bg));
385
-
386
- &--open {
387
- display: block;
388
- }
389
- }
390
-
391
- // Level 2 submenu links
392
- &__submenu > &__item > &__link {
393
- padding-inline-start: $sidebar-submenu-indent; // RTL: flips to padding-right
394
- font-size: $font-size-md;
395
- border-inline-end: $border-width-medium solid transparent; // RTL: flips to border-left
396
-
397
- &:hover {
398
- background-color: var(--pc-sidebar-submenu-hover-bg, var(--base-hover-bg));
399
- }
400
-
401
- &--active {
402
- background-color: var(--pc-sidebar-submenu-active-bg, var(--base-active-bg));
403
- color: var(--pc-sidebar-submenu-active-text, var(--base-text-color-1));
404
- border-inline-end-color: var(--pc-accent); // RTL: flips to border-left-color
405
- }
406
- }
407
-
408
- // Level 2 submenu toggles (for third level)
409
- &__submenu > &__item > &__toggle {
410
- padding-inline-start: $sidebar-submenu-indent; // RTL: flips to padding-right
411
- font-size: $font-size-md;
412
- }
413
-
414
- // Level 3 submenu links
415
- &__submenu &__submenu > &__item > &__link {
416
- padding-inline-start: calc($sidebar-submenu-indent * $sidebar-submenu-indent-multiplier); // RTL: flips
417
- font-size: $font-size-sm;
418
- border-inline-end: $border-width-base solid transparent; // RTL: flips to border-left
419
-
420
- &--active {
421
- border-inline-end-color: var(--pc-accent); // RTL: flips to border-left-color
422
- }
423
- }
424
-
425
- // Chevron icon for toggle buttons. Rendered as an SVG mask painted in
426
- // currentColor (was a bare "›" text glyph, which rendered inconsistently
427
- // across platforms/fonts). Reads the shared --base-icon-chevron so it aligns
428
- // with pure-admin + the web components; --pa-icon-chevron (core) lets pure-admin
429
- // override, and #{$base-icon-chevron} is the standalone literal fallback.
430
- // `font-size: 0` collapses any legacy "›" still present in older markup so it
431
- // never double-renders alongside the mask.
432
- &__chevron {
433
- margin-inline-start: auto; // RTL: flips to margin-right: auto
434
- inline-size: 1.6rem;
435
- block-size: 1.6rem;
436
- flex-shrink: 0;
437
- font-size: 0;
438
- background-color: currentColor;
439
- -webkit-mask: var(--base-icon-chevron, #{$base-icon-chevron}) center / 1.2rem no-repeat;
440
- mask: var(--base-icon-chevron, #{$base-icon-chevron}) center / 1.2rem no-repeat;
441
-
442
- body.loaded & {
443
- transition: transform $transition-fast $easing-snappy;
444
- }
445
-
446
- // Chevron points right (›) in LTR, left (‹) in RTL
447
- // When open, rotates 90deg clockwise in LTR, -90deg (counter-clockwise) in RTL.
448
- // Scoped to the item's OWN chevron (direct toggle child) — a plain descendant
449
- // selector would also rotate nested items' chevrons whenever an ANCESTOR is
450
- // open, so a closed sub-group would wrongly show a "down" chevron.
451
- .pc-sidebar__item--open > .pc-sidebar__toggle > & {
452
- transform: rotate(90deg);
453
- }
454
-
455
- [dir="rtl"] .pc-sidebar__item--open > .pc-sidebar__toggle > & {
456
- transform: rotate(-90deg);
457
- }
458
- }
459
- }
1
+ /* ========================================
2
+ Sidebar Component
3
+ Sidebar navigation with nested submenus
4
+ ======================================== */
5
+ @use 'variables/index' as *;
6
+ @use 'resize-handle' as *;
7
+
8
+ // Local CSS variables for sidebar (runtime state, modified by JS for resize)
9
+ :root {
10
+ --pc-local-sidebar-width: #{$sidebar-width};
11
+ --pc-local-sidebar-min-width: 18rem;
12
+ --pc-local-sidebar-max-width: 50rem;
13
+ }
14
+
15
+ // Sidebar layout element - width in :where() for low specificity (utility classes can override)
16
+ :where(.pc-layout__sidebar) {
17
+ width: var(--pc-local-sidebar-width);
18
+ }
19
+
20
+ .pc-layout__sidebar {
21
+ background-color: var(--pc-sidebar-bg, var(--base-page-bg));
22
+ border-inline-end: $border-width-base solid var(--pc-border-color); // RTL: flips to border-left
23
+ flex-shrink: 0;
24
+ position: relative;
25
+ }
26
+
27
+ // Resize handle on the sidebar edge. Shows the shared resize knob (see
28
+ // _resize-handle.scss) so it's the same control as the splitter gutter — slim on
29
+ // desktop, chunkier on small viewports. The knob is sticky at mid-viewport so it
30
+ // stays on screen on long, page-height sidebars.
31
+ .pc-sidebar-resize {
32
+ position: absolute;
33
+ top: 0;
34
+ inset-inline-end: 0; // RTL: flips to left
35
+ width: 6px;
36
+ height: 100%;
37
+ cursor: ew-resize;
38
+ z-index: 100;
39
+ // Own the touch gesture. Without this the browser's touch-action heuristic can
40
+ // interpret a drag as a scroll/pan and CANCEL the touch sequence after
41
+ // touchstart — so touchmove never reaches the resize handler and the drag
42
+ // silently dies. Harmless for mouse (mousedown path is unaffected).
43
+ touch-action: none;
44
+ // Flex so the sticky knob can pin to the inline-end edge and ride the viewport.
45
+ display: flex;
46
+ justify-content: flex-end;
47
+ align-items: flex-start;
48
+
49
+ // Shared grab-knob. `position: sticky; top: 50vh` keeps it at the middle of the
50
+ // VISIBLE viewport, not the middle of the sidebar's full length — on a long
51
+ // page the sidebar stretches to the whole scroll height, so a plain top:50%
52
+ // knob ends up far off-screen. flex-end pins it to the inline-end edge;
53
+ // translateX(50%) straddles it onto the sidebar's border line; translateY(-50%)
54
+ // centres it on the 50vh sticky point.
55
+ &::before {
56
+ @include pa-resize-knob($axis: block);
57
+ position: sticky;
58
+ top: 50vh;
59
+ transform: translate(50%, -50%);
60
+ }
61
+
62
+ &:hover::before,
63
+ &--active::before {
64
+ @include pa-resize-knob-active;
65
+ }
66
+ }
67
+
68
+ // Touch: widen only the (invisible) grab area to a finger-sized target. The knob
69
+ // LOOK is viewport-driven (handled by the shared mixin), so it's unchanged here.
70
+ @media (pointer: coarse) {
71
+ .pc-sidebar-resize {
72
+ width: 24px;
73
+ }
74
+ }
75
+
76
+ // Sticky LAYOUT mode: the whole page doesn't scroll (body overflow:hidden) and
77
+ // the sidebar is already ~viewport height, where position:sticky never engages.
78
+ // Absolute-centre the knob instead — 50% of a viewport-height sidebar IS the
79
+ // viewport centre.
80
+ body.pc-layout--sticky .pc-sidebar-resize::before {
81
+ position: absolute;
82
+ top: 50%;
83
+ inset-inline-end: 0;
84
+ }
85
+
86
+ // Disable text selection during resize
87
+ body.pc-sidebar-resizing {
88
+ cursor: ew-resize !important;
89
+ user-select: none;
90
+
91
+ * {
92
+ cursor: ew-resize !important;
93
+ }
94
+ }
95
+
96
+ // Sticky mode - sidebar scrolls
97
+ body.pc-layout--sticky .pc-layout__sidebar {
98
+ overflow-y: auto;
99
+ overflow-y: overlay; // Chromium: scrollbar overlays content, doesn't reduce width
100
+ overflow-x: visible; // Allow tooltips to escape (right in LTR, left in RTL)
101
+ }
102
+
103
+ // Sidebar component (separate BEM block)
104
+ .pc-sidebar {
105
+ &__nav {
106
+ padding: $spacing-base 0;
107
+
108
+ ul {
109
+ list-style: none;
110
+ margin: 0;
111
+ padding: 0;
112
+ }
113
+ }
114
+
115
+ &__item {
116
+ // Base styles for all sidebar items (regular links and submenu toggles)
117
+ }
118
+
119
+ // Flat section heading — a non-interactive group label ("PROJECT",
120
+ // "GUIDES", "LIVE DEMOS"). Use when the nav is grouped but the groups are
121
+ // NOT collapsible; for collapsible groups use __toggle + __submenu instead.
122
+ // Rendered as an <li class="pc-sidebar__section"> inside __nav's <ul>, so it
123
+ // sits in the list flow without a bullet (the <ul> already resets list-style).
124
+ &__section {
125
+ padding: $spacing-md $spacing-base $spacing-xs; // 16px inset aligns with __link text
126
+ font-size: $font-size-xs;
127
+ font-weight: $font-weight-semibold;
128
+ text-transform: uppercase;
129
+ letter-spacing: 0.05em;
130
+ color: var(--pc-sidebar-text-secondary, var(--base-text-color-2));
131
+ user-select: none;
132
+
133
+ // First heading shouldn't push the whole nav down
134
+ &:first-child {
135
+ padding-top: $spacing-xs;
136
+ }
137
+ }
138
+
139
+ // Thin horizontal rule separating groups of items — e.g. between nav items
140
+ // folded in from the navbar (fit.js nav-collapse, sidebar sink) and the
141
+ // sidebar's own links. Rendered as an <li class="pc-sidebar__divider"> so it
142
+ // sits in the list flow; carries no text.
143
+ &__divider {
144
+ height: 0;
145
+ margin: $spacing-xs $spacing-base;
146
+ padding: 0;
147
+ border: 0;
148
+ border-top: $border-width-base solid var(--pc-border-color);
149
+ list-style: none;
150
+ }
151
+
152
+ &__link {
153
+ display: flex;
154
+ align-items: center;
155
+ gap: $sidebar-item-gap;
156
+ padding: $sidebar-padding;
157
+ color: var(--pc-sidebar-text-secondary, var(--base-text-color-2));
158
+ text-decoration: none;
159
+ font-weight: $font-weight-medium;
160
+ border-inline-end: $border-width-thick solid transparent; // RTL: flips to border-left
161
+
162
+ &:hover {
163
+ color: var(--pc-sidebar-text, var(--base-text-color-1));
164
+ background-color: var(--pc-accent-light);
165
+ }
166
+
167
+ &--active {
168
+ color: var(--pc-accent);
169
+ background-color: var(--pc-accent-hover);
170
+ border-inline-end-color: var(--pc-accent); // RTL: flips to border-left-color
171
+ font-weight: $font-weight-semibold;
172
+ }
173
+ }
174
+
175
+ // Search affordance at the top of the sidebar — a TRIGGER (a consumer wires it
176
+ // to open a command palette / their own search), rendered as a framed
177
+ // faux-input row so it reads as "search" rather than another nav link:
178
+ // magnifier icon + muted label, inset from the sidebar edges. In icon-collapse
179
+ // mode it sheds the frame and centres to just the icon, exactly like the links
180
+ // below (its .pc-sidebar__label is hidden by the shared icon-collapse rule).
181
+ &__search {
182
+ display: flex;
183
+ align-items: center;
184
+ gap: $sidebar-item-gap;
185
+ width: calc(100% - #{$spacing-sm} * 2);
186
+ margin: $spacing-sm;
187
+ // Sized on the md input height (35px) to match the navbar search boxes,
188
+ // rather than the icon + $sidebar-padding metric of a nav row (40px). Trade-off:
189
+ // it sits ~5px shorter than the links below it. Block padding is dropped (fixed
190
+ // height + align-items:center owns the vertical).
191
+ // Inline padding: right = $spacing-base (the nav-row inset); left = $spacing-sm
192
+ // so that margin ($spacing-sm) + padding-left ($spacing-sm) = $spacing-base —
193
+ // landing the magnifier exactly where the nav-row icons below it sit, instead
194
+ // of one box-margin too far in.
195
+ height: #{$base-input-size-md-height}rem;
196
+ padding: 0 $spacing-base 0 $spacing-sm;
197
+ color: var(--pc-text-color-2);
198
+ background-color: var(--base-input-bg);
199
+ border: $border-width-base solid var(--pc-border-color);
200
+ border-radius: var(--pc-border-radius);
201
+ text-align: start; // RTL: flips to right
202
+ font-weight: $font-weight-medium;
203
+ cursor: pointer;
204
+
205
+ // Honor the standard HTML hidden attribute (display:flex above beats the UA
206
+ // `[hidden]{display:none}` rule otherwise).
207
+ &[hidden] {
208
+ display: none;
209
+ }
210
+
211
+ &:hover {
212
+ // $accent-color (foundation) — was $input-focus-border-color, an exact
213
+ // alias; inlined so the shell no longer depends on the component layer
214
+ // (variables/_components.scss now lives in pure-admin-core).
215
+ border-color: $accent-color;
216
+ // This framed box sits on the INPUT surface (--base-input-bg above), NOT
217
+ // the sidebar surface — so emphasise with the input text colour, not
218
+ // --pc-sidebar-text. In themes with a mode-independent sidebar (e.g.
219
+ // express: always-black sidebar → --pc-sidebar-text is white) the old
220
+ // sidebar-text hover went white-on-white on the light input bg.
221
+ color: var(--base-input-color);
222
+ }
223
+
224
+ // Collapsed rail ONLY (icon-collapse mode AND actually collapsed, i.e.
225
+ // body.sidebar-hidden): drop the frame, icon-only, matching link padding so
226
+ // the icon lands in the same column as every other sidebar icon. Must be
227
+ // gated by .sidebar-hidden — .pc-layout__sidebar--icon-collapse is the MODE
228
+ // class and stays on the element while the rail is expanded, so an unscoped
229
+ // rule would strip the search frame in the expanded state too.
230
+ .sidebar-hidden .pc-layout__sidebar--icon-collapse & {
231
+ width: auto;
232
+ margin: 0;
233
+ padding: $sidebar-padding;
234
+ background: transparent;
235
+ border-color: transparent;
236
+ border-radius: 0;
237
+ }
238
+
239
+ // Type-and-go variant: the framed row hosts a <form> whose native GET submit
240
+ // redirects to a results page on Enter — instead of a button that opens the
241
+ // palette. A single text field submits on Enter with no submit button; the
242
+ // magnifier is a type="submit" so that in icon-collapse mode (field hidden)
243
+ // clicking it still submits — an empty query lands on the bare results page.
244
+ // Keeps the base $sidebar-padding frame untouched so the outer box is
245
+ // identical to the trigger button; the submit icon mirrors .pc-sidebar__icon
246
+ // (fixed size, line-height 1) so it drives the row height exactly like a link,
247
+ // and the borderless field just fills the rest. (Don't feed $sidebar-padding —
248
+ // a two-value 0.5rem/1rem token — to padding-block: it maps to top/bottom and
249
+ // inflates the box.)
250
+ &--input {
251
+ cursor: text;
252
+
253
+ .pc-sidebar__search-icon {
254
+ width: $sidebar-icon-size;
255
+ min-width: $sidebar-icon-size;
256
+ height: $sidebar-icon-size;
257
+ flex-shrink: 0;
258
+ padding: 0;
259
+ border: none;
260
+ cursor: pointer;
261
+ color: inherit;
262
+ // Masked magnifier painted in currentColor — was a 🔍 text emoji, which
263
+ // rendered inconsistently across platforms/fonts (the same reason the
264
+ // sidebar chevron is masked above). Reads the shared --base-icon-search
265
+ // so it matches pure-admin's .pa-icon--search and the navbar search;
266
+ // #{$base-icon-search} is the standalone literal fallback. font-size:0
267
+ // collapses any legacy 🔍 still in markup so it never double-renders.
268
+ font-size: 0;
269
+ background-color: currentColor;
270
+ -webkit-mask: var(--base-icon-search, #{$base-icon-search}) center / #{$font-size-base} no-repeat;
271
+ mask: var(--base-icon-search, #{$base-icon-search}) center / #{$font-size-base} no-repeat;
272
+ }
273
+
274
+ .pc-sidebar__search-field {
275
+ flex: 1;
276
+ min-width: 0;
277
+ border: none;
278
+ outline: none;
279
+ background: transparent;
280
+ // The field shows through to the framed box's --base-input-bg surface,
281
+ // so its text must use the input text colour (--base-input-color), not
282
+ // the sidebar text colour. --pc-sidebar-text is tuned for the sidebar
283
+ // surface, which can be mode-independent (express: always-black sidebar
284
+ // → white sidebar text) and then renders white-on-white on a light
285
+ // input bg. --base-input-color is emitted per-mode alongside
286
+ // --base-input-bg, so the pair is always legible.
287
+ color: var(--base-input-color);
288
+ font: inherit;
289
+ padding: 0;
290
+
291
+ &::placeholder {
292
+ color: var(--pc-text-color-2);
293
+ opacity: $opacity-muted;
294
+ }
295
+
296
+ &::-webkit-search-decoration,
297
+ &::-webkit-search-cancel-button {
298
+ -webkit-appearance: none;
299
+ }
300
+ }
301
+ }
302
+
303
+ // Collapsed rail ONLY: the input variant sheds its field and centres on the
304
+ // submit icon; clicking it submits with an empty query (the bare results
305
+ // page). Gated by .sidebar-hidden so the field reappears when the icon-collapse
306
+ // rail is expanded (see frame rule above for why the mode class isn't enough).
307
+ .sidebar-hidden .pc-layout__sidebar--icon-collapse &--input {
308
+ justify-content: center;
309
+
310
+ .pc-sidebar__search-field {
311
+ display: none;
312
+ }
313
+ }
314
+ }
315
+
316
+ &__toggle {
317
+ // For submenu toggle buttons (non-link items)
318
+ display: flex;
319
+ align-items: center;
320
+ gap: $sidebar-item-gap;
321
+ padding: $sidebar-padding;
322
+ color: var(--pc-sidebar-text-secondary, var(--base-text-color-2));
323
+ background: none;
324
+ border: none;
325
+ width: 100%;
326
+ text-align: start; // RTL: flips to right
327
+ font-weight: $font-weight-medium;
328
+ cursor: pointer;
329
+
330
+ &:hover {
331
+ color: var(--pc-sidebar-text, var(--base-text-color-1));
332
+ background-color: var(--pc-accent-light);
333
+ }
334
+
335
+ &--active {
336
+ color: var(--pc-accent);
337
+ background-color: var(--pc-accent-hover);
338
+ }
339
+ }
340
+
341
+ // Icon and label styles
342
+ &__icon {
343
+ width: $sidebar-icon-size;
344
+ min-width: $sidebar-icon-size;
345
+ height: $sidebar-icon-size;
346
+ display: flex;
347
+ align-items: center;
348
+ justify-content: center;
349
+ flex-shrink: 0;
350
+ font-size: $font-size-base;
351
+ text-align: center;
352
+ line-height: 1; // Prevent emoji baseline shift
353
+ }
354
+
355
+ &__label {
356
+ flex: 1;
357
+ overflow: hidden;
358
+ text-overflow: ellipsis;
359
+ white-space: nowrap;
360
+
361
+ body.loaded & {
362
+ transition: opacity $transition-normal $easing-snappy, width $transition-normal $easing-snappy;
363
+ }
364
+ }
365
+
366
+ // Submenu styles - Level 2
367
+ &__submenu {
368
+ list-style: none;
369
+ margin: 0;
370
+ padding: 0;
371
+ display: none;
372
+ background-color: var(--pc-sidebar-submenu-bg, var(--base-subtle-bg));
373
+
374
+ &--open {
375
+ display: block;
376
+ }
377
+ }
378
+
379
+ // Level 2 submenu links
380
+ &__submenu > &__item > &__link {
381
+ padding-inline-start: $sidebar-submenu-indent; // RTL: flips to padding-right
382
+ font-size: $font-size-md;
383
+ border-inline-end: $border-width-medium solid transparent; // RTL: flips to border-left
384
+
385
+ &:hover {
386
+ background-color: var(--pc-sidebar-submenu-hover-bg, var(--base-hover-bg));
387
+ }
388
+
389
+ &--active {
390
+ background-color: var(--pc-sidebar-submenu-active-bg, var(--base-active-bg));
391
+ color: var(--pc-sidebar-submenu-active-text, var(--base-text-color-1));
392
+ border-inline-end-color: var(--pc-accent); // RTL: flips to border-left-color
393
+ }
394
+ }
395
+
396
+ // Level 2 submenu toggles (for third level)
397
+ &__submenu > &__item > &__toggle {
398
+ padding-inline-start: $sidebar-submenu-indent; // RTL: flips to padding-right
399
+ font-size: $font-size-md;
400
+ }
401
+
402
+ // Level 3 submenu links
403
+ &__submenu &__submenu > &__item > &__link {
404
+ padding-inline-start: calc($sidebar-submenu-indent * $sidebar-submenu-indent-multiplier); // RTL: flips
405
+ font-size: $font-size-sm;
406
+ border-inline-end: $border-width-base solid transparent; // RTL: flips to border-left
407
+
408
+ &--active {
409
+ border-inline-end-color: var(--pc-accent); // RTL: flips to border-left-color
410
+ }
411
+ }
412
+
413
+ // Chevron icon for toggle buttons. Rendered as an SVG mask painted in
414
+ // currentColor (was a bare "›" text glyph, which rendered inconsistently
415
+ // across platforms/fonts). Reads the shared --base-icon-chevron so it aligns
416
+ // with pure-admin + the web components; --pa-icon-chevron (core) lets pure-admin
417
+ // override, and #{$base-icon-chevron} is the standalone literal fallback.
418
+ // `font-size: 0` collapses any legacy "›" still present in older markup so it
419
+ // never double-renders alongside the mask.
420
+ &__chevron {
421
+ margin-inline-start: auto; // RTL: flips to margin-right: auto
422
+ inline-size: 1.6rem;
423
+ block-size: 1.6rem;
424
+ flex-shrink: 0;
425
+ font-size: 0;
426
+ background-color: currentColor;
427
+ -webkit-mask: var(--base-icon-chevron, #{$base-icon-chevron}) center / 1.2rem no-repeat;
428
+ mask: var(--base-icon-chevron, #{$base-icon-chevron}) center / 1.2rem no-repeat;
429
+
430
+ body.loaded & {
431
+ transition: transform $transition-fast $easing-snappy;
432
+ }
433
+
434
+ // Chevron points right (›) in LTR, left (‹) in RTL
435
+ // When open, rotates 90deg clockwise in LTR, -90deg (counter-clockwise) in RTL.
436
+ // Scoped to the item's OWN chevron (direct toggle child) — a plain descendant
437
+ // selector would also rotate nested items' chevrons whenever an ANCESTOR is
438
+ // open, so a closed sub-group would wrongly show a "down" chevron.
439
+ .pc-sidebar__item--open > .pc-sidebar__toggle > & {
440
+ transform: rotate(90deg);
441
+ }
442
+
443
+ [dir="rtl"] .pc-sidebar__item--open > .pc-sidebar__toggle > & {
444
+ transform: rotate(-90deg);
445
+ }
446
+ }
447
+ }