@keenmate/pure-css 1.0.0-rc08 → 1.0.0-rc09

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,443 +1,443 @@
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(--pc-input-bg, 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
- color: var(--pc-sidebar-text, var(--base-text-color-1));
225
- }
226
-
227
- // Collapsed rail ONLY (icon-collapse mode AND actually collapsed, i.e.
228
- // body.sidebar-hidden): drop the frame, icon-only, matching link padding so
229
- // the icon lands in the same column as every other sidebar icon. Must be
230
- // gated by .sidebar-hidden — .pc-layout__sidebar--icon-collapse is the MODE
231
- // class and stays on the element while the rail is expanded, so an unscoped
232
- // rule would strip the search frame in the expanded state too.
233
- .sidebar-hidden .pc-layout__sidebar--icon-collapse & {
234
- width: auto;
235
- margin: 0;
236
- padding: $sidebar-padding;
237
- background: transparent;
238
- border-color: transparent;
239
- border-radius: 0;
240
- }
241
-
242
- // Type-and-go variant: the framed row hosts a <form> whose native GET submit
243
- // redirects to a results page on Enter — instead of a button that opens the
244
- // palette. A single text field submits on Enter with no submit button; the
245
- // magnifier is a type="submit" so that in icon-collapse mode (field hidden)
246
- // clicking it still submits — an empty query lands on the bare results page.
247
- // Keeps the base $sidebar-padding frame untouched so the outer box is
248
- // identical to the trigger button; the submit icon mirrors .pc-sidebar__icon
249
- // (fixed size, line-height 1) so it drives the row height exactly like a link,
250
- // and the borderless field just fills the rest. (Don't feed $sidebar-padding —
251
- // a two-value 0.5rem/1rem token — to padding-block: it maps to top/bottom and
252
- // inflates the box.)
253
- &--input {
254
- cursor: text;
255
-
256
- .pc-sidebar__search-icon {
257
- width: $sidebar-icon-size;
258
- min-width: $sidebar-icon-size;
259
- height: $sidebar-icon-size;
260
- display: flex;
261
- align-items: center;
262
- justify-content: center;
263
- flex-shrink: 0;
264
- padding: 0;
265
- border: none;
266
- background: transparent;
267
- color: inherit;
268
- font-size: $font-size-base;
269
- line-height: 1; // prevent emoji baseline shift, matching __icon
270
- cursor: pointer;
271
- }
272
-
273
- .pc-sidebar__search-field {
274
- flex: 1;
275
- min-width: 0;
276
- border: none;
277
- outline: none;
278
- background: transparent;
279
- color: var(--pc-sidebar-text, var(--base-text-color-1));
280
- font: inherit;
281
- padding: 0;
282
-
283
- &::placeholder {
284
- color: var(--pc-text-color-2);
285
- opacity: $opacity-muted;
286
- }
287
-
288
- &::-webkit-search-decoration,
289
- &::-webkit-search-cancel-button {
290
- -webkit-appearance: none;
291
- }
292
- }
293
- }
294
-
295
- // Collapsed rail ONLY: the input variant sheds its field and centres on the
296
- // submit icon; clicking it submits with an empty query (the bare results
297
- // page). Gated by .sidebar-hidden so the field reappears when the icon-collapse
298
- // rail is expanded (see frame rule above for why the mode class isn't enough).
299
- .sidebar-hidden .pc-layout__sidebar--icon-collapse &--input {
300
- justify-content: center;
301
-
302
- .pc-sidebar__search-field {
303
- display: none;
304
- }
305
- }
306
- }
307
-
308
- &__toggle {
309
- // For submenu toggle buttons (non-link items)
310
- display: flex;
311
- align-items: center;
312
- gap: $sidebar-item-gap;
313
- padding: $sidebar-padding;
314
- color: var(--pc-sidebar-text-secondary, var(--base-text-color-2));
315
- background: none;
316
- border: none;
317
- width: 100%;
318
- text-align: start; // RTL: flips to right
319
- font-weight: $font-weight-medium;
320
- cursor: pointer;
321
-
322
- body.loaded & {
323
- transition: all $transition-fast $easing-snappy;
324
- }
325
-
326
- &:hover {
327
- color: var(--pc-sidebar-text, var(--base-text-color-1));
328
- background-color: var(--pc-accent-light);
329
- }
330
-
331
- &--active {
332
- color: var(--pc-accent);
333
- background-color: var(--pc-accent-hover);
334
- }
335
- }
336
-
337
- // Icon and label styles
338
- &__icon {
339
- width: $sidebar-icon-size;
340
- min-width: $sidebar-icon-size;
341
- height: $sidebar-icon-size;
342
- display: flex;
343
- align-items: center;
344
- justify-content: center;
345
- flex-shrink: 0;
346
- font-size: $font-size-base;
347
- text-align: center;
348
- line-height: 1; // Prevent emoji baseline shift
349
- }
350
-
351
- &__label {
352
- flex: 1;
353
- overflow: hidden;
354
- text-overflow: ellipsis;
355
- white-space: nowrap;
356
-
357
- body.loaded & {
358
- transition: opacity $transition-normal $easing-snappy, width $transition-normal $easing-snappy;
359
- }
360
- }
361
-
362
- // Submenu styles - Level 2
363
- &__submenu {
364
- list-style: none;
365
- margin: 0;
366
- padding: 0;
367
- display: none;
368
- background-color: var(--pc-sidebar-submenu-bg, var(--base-subtle-bg));
369
-
370
- &--open {
371
- display: block;
372
- }
373
- }
374
-
375
- // Level 2 submenu links
376
- &__submenu > &__item > &__link {
377
- padding-inline-start: $sidebar-submenu-indent; // RTL: flips to padding-right
378
- font-size: $font-size-md;
379
- border-inline-end: $border-width-medium solid transparent; // RTL: flips to border-left
380
-
381
- &:hover {
382
- background-color: var(--pc-sidebar-submenu-hover-bg, var(--base-hover-bg));
383
- }
384
-
385
- &--active {
386
- background-color: var(--pc-sidebar-submenu-active-bg, var(--base-active-bg));
387
- color: var(--pc-sidebar-submenu-active-text, var(--base-text-color-1));
388
- border-inline-end-color: var(--pc-accent); // RTL: flips to border-left-color
389
- }
390
- }
391
-
392
- // Level 2 submenu toggles (for third level)
393
- &__submenu > &__item > &__toggle {
394
- padding-inline-start: $sidebar-submenu-indent; // RTL: flips to padding-right
395
- font-size: $font-size-md;
396
- }
397
-
398
- // Level 3 submenu links
399
- &__submenu &__submenu > &__item > &__link {
400
- padding-inline-start: calc($sidebar-submenu-indent * $sidebar-submenu-indent-multiplier); // RTL: flips
401
- font-size: $font-size-sm;
402
- border-inline-end: $border-width-base solid transparent; // RTL: flips to border-left
403
-
404
- &--active {
405
- border-inline-end-color: var(--pc-accent); // RTL: flips to border-left-color
406
- }
407
- }
408
-
409
- // Chevron icon for toggle buttons. Rendered as an SVG mask painted in
410
- // currentColor (was a bare "›" text glyph, which rendered inconsistently
411
- // across platforms/fonts). Reads the shared --base-icon-chevron so it aligns
412
- // with pure-admin + the web components; --pc-icon-chevron lets pure-admin
413
- // override, and #{$base-icon-chevron} is the standalone literal fallback.
414
- // `font-size: 0` collapses any legacy "›" still present in older markup so it
415
- // never double-renders alongside the mask.
416
- &__chevron {
417
- margin-inline-start: auto; // RTL: flips to margin-right: auto
418
- inline-size: 1.6rem;
419
- block-size: 1.6rem;
420
- flex-shrink: 0;
421
- font-size: 0;
422
- background-color: currentColor;
423
- -webkit-mask: var(--pc-icon-chevron, var(--base-icon-chevron, #{$base-icon-chevron})) center / 1.2rem no-repeat;
424
- mask: var(--pc-icon-chevron, var(--base-icon-chevron, #{$base-icon-chevron})) center / 1.2rem no-repeat;
425
-
426
- body.loaded & {
427
- transition: transform $transition-fast $easing-snappy;
428
- }
429
-
430
- // Chevron points right (›) in LTR, left (‹) in RTL
431
- // When open, rotates 90deg clockwise in LTR, -90deg (counter-clockwise) in RTL.
432
- // Scoped to the item's OWN chevron (direct toggle child) — a plain descendant
433
- // selector would also rotate nested items' chevrons whenever an ANCESTOR is
434
- // open, so a closed sub-group would wrongly show a "down" chevron.
435
- .pc-sidebar__item--open > .pc-sidebar__toggle > & {
436
- transform: rotate(90deg);
437
- }
438
-
439
- [dir="rtl"] .pc-sidebar__item--open > .pc-sidebar__toggle > & {
440
- transform: rotate(-90deg);
441
- }
442
- }
443
- }
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
+ color: var(--pc-sidebar-text, var(--base-text-color-1));
225
+ }
226
+
227
+ // Collapsed rail ONLY (icon-collapse mode AND actually collapsed, i.e.
228
+ // body.sidebar-hidden): drop the frame, icon-only, matching link padding so
229
+ // the icon lands in the same column as every other sidebar icon. Must be
230
+ // gated by .sidebar-hidden — .pc-layout__sidebar--icon-collapse is the MODE
231
+ // class and stays on the element while the rail is expanded, so an unscoped
232
+ // rule would strip the search frame in the expanded state too.
233
+ .sidebar-hidden .pc-layout__sidebar--icon-collapse & {
234
+ width: auto;
235
+ margin: 0;
236
+ padding: $sidebar-padding;
237
+ background: transparent;
238
+ border-color: transparent;
239
+ border-radius: 0;
240
+ }
241
+
242
+ // Type-and-go variant: the framed row hosts a <form> whose native GET submit
243
+ // redirects to a results page on Enter — instead of a button that opens the
244
+ // palette. A single text field submits on Enter with no submit button; the
245
+ // magnifier is a type="submit" so that in icon-collapse mode (field hidden)
246
+ // clicking it still submits — an empty query lands on the bare results page.
247
+ // Keeps the base $sidebar-padding frame untouched so the outer box is
248
+ // identical to the trigger button; the submit icon mirrors .pc-sidebar__icon
249
+ // (fixed size, line-height 1) so it drives the row height exactly like a link,
250
+ // and the borderless field just fills the rest. (Don't feed $sidebar-padding —
251
+ // a two-value 0.5rem/1rem token — to padding-block: it maps to top/bottom and
252
+ // inflates the box.)
253
+ &--input {
254
+ cursor: text;
255
+
256
+ .pc-sidebar__search-icon {
257
+ width: $sidebar-icon-size;
258
+ min-width: $sidebar-icon-size;
259
+ height: $sidebar-icon-size;
260
+ display: flex;
261
+ align-items: center;
262
+ justify-content: center;
263
+ flex-shrink: 0;
264
+ padding: 0;
265
+ border: none;
266
+ background: transparent;
267
+ color: inherit;
268
+ font-size: $font-size-base;
269
+ line-height: 1; // prevent emoji baseline shift, matching __icon
270
+ cursor: pointer;
271
+ }
272
+
273
+ .pc-sidebar__search-field {
274
+ flex: 1;
275
+ min-width: 0;
276
+ border: none;
277
+ outline: none;
278
+ background: transparent;
279
+ color: var(--pc-sidebar-text, var(--base-text-color-1));
280
+ font: inherit;
281
+ padding: 0;
282
+
283
+ &::placeholder {
284
+ color: var(--pc-text-color-2);
285
+ opacity: $opacity-muted;
286
+ }
287
+
288
+ &::-webkit-search-decoration,
289
+ &::-webkit-search-cancel-button {
290
+ -webkit-appearance: none;
291
+ }
292
+ }
293
+ }
294
+
295
+ // Collapsed rail ONLY: the input variant sheds its field and centres on the
296
+ // submit icon; clicking it submits with an empty query (the bare results
297
+ // page). Gated by .sidebar-hidden so the field reappears when the icon-collapse
298
+ // rail is expanded (see frame rule above for why the mode class isn't enough).
299
+ .sidebar-hidden .pc-layout__sidebar--icon-collapse &--input {
300
+ justify-content: center;
301
+
302
+ .pc-sidebar__search-field {
303
+ display: none;
304
+ }
305
+ }
306
+ }
307
+
308
+ &__toggle {
309
+ // For submenu toggle buttons (non-link items)
310
+ display: flex;
311
+ align-items: center;
312
+ gap: $sidebar-item-gap;
313
+ padding: $sidebar-padding;
314
+ color: var(--pc-sidebar-text-secondary, var(--base-text-color-2));
315
+ background: none;
316
+ border: none;
317
+ width: 100%;
318
+ text-align: start; // RTL: flips to right
319
+ font-weight: $font-weight-medium;
320
+ cursor: pointer;
321
+
322
+ body.loaded & {
323
+ transition: all $transition-fast $easing-snappy;
324
+ }
325
+
326
+ &:hover {
327
+ color: var(--pc-sidebar-text, var(--base-text-color-1));
328
+ background-color: var(--pc-accent-light);
329
+ }
330
+
331
+ &--active {
332
+ color: var(--pc-accent);
333
+ background-color: var(--pc-accent-hover);
334
+ }
335
+ }
336
+
337
+ // Icon and label styles
338
+ &__icon {
339
+ width: $sidebar-icon-size;
340
+ min-width: $sidebar-icon-size;
341
+ height: $sidebar-icon-size;
342
+ display: flex;
343
+ align-items: center;
344
+ justify-content: center;
345
+ flex-shrink: 0;
346
+ font-size: $font-size-base;
347
+ text-align: center;
348
+ line-height: 1; // Prevent emoji baseline shift
349
+ }
350
+
351
+ &__label {
352
+ flex: 1;
353
+ overflow: hidden;
354
+ text-overflow: ellipsis;
355
+ white-space: nowrap;
356
+
357
+ body.loaded & {
358
+ transition: opacity $transition-normal $easing-snappy, width $transition-normal $easing-snappy;
359
+ }
360
+ }
361
+
362
+ // Submenu styles - Level 2
363
+ &__submenu {
364
+ list-style: none;
365
+ margin: 0;
366
+ padding: 0;
367
+ display: none;
368
+ background-color: var(--pc-sidebar-submenu-bg, var(--base-subtle-bg));
369
+
370
+ &--open {
371
+ display: block;
372
+ }
373
+ }
374
+
375
+ // Level 2 submenu links
376
+ &__submenu > &__item > &__link {
377
+ padding-inline-start: $sidebar-submenu-indent; // RTL: flips to padding-right
378
+ font-size: $font-size-md;
379
+ border-inline-end: $border-width-medium solid transparent; // RTL: flips to border-left
380
+
381
+ &:hover {
382
+ background-color: var(--pc-sidebar-submenu-hover-bg, var(--base-hover-bg));
383
+ }
384
+
385
+ &--active {
386
+ background-color: var(--pc-sidebar-submenu-active-bg, var(--base-active-bg));
387
+ color: var(--pc-sidebar-submenu-active-text, var(--base-text-color-1));
388
+ border-inline-end-color: var(--pc-accent); // RTL: flips to border-left-color
389
+ }
390
+ }
391
+
392
+ // Level 2 submenu toggles (for third level)
393
+ &__submenu > &__item > &__toggle {
394
+ padding-inline-start: $sidebar-submenu-indent; // RTL: flips to padding-right
395
+ font-size: $font-size-md;
396
+ }
397
+
398
+ // Level 3 submenu links
399
+ &__submenu &__submenu > &__item > &__link {
400
+ padding-inline-start: calc($sidebar-submenu-indent * $sidebar-submenu-indent-multiplier); // RTL: flips
401
+ font-size: $font-size-sm;
402
+ border-inline-end: $border-width-base solid transparent; // RTL: flips to border-left
403
+
404
+ &--active {
405
+ border-inline-end-color: var(--pc-accent); // RTL: flips to border-left-color
406
+ }
407
+ }
408
+
409
+ // Chevron icon for toggle buttons. Rendered as an SVG mask painted in
410
+ // currentColor (was a bare "›" text glyph, which rendered inconsistently
411
+ // across platforms/fonts). Reads the shared --base-icon-chevron so it aligns
412
+ // with pure-admin + the web components; --pa-icon-chevron (core) lets pure-admin
413
+ // override, and #{$base-icon-chevron} is the standalone literal fallback.
414
+ // `font-size: 0` collapses any legacy "›" still present in older markup so it
415
+ // never double-renders alongside the mask.
416
+ &__chevron {
417
+ margin-inline-start: auto; // RTL: flips to margin-right: auto
418
+ inline-size: 1.6rem;
419
+ block-size: 1.6rem;
420
+ flex-shrink: 0;
421
+ font-size: 0;
422
+ background-color: currentColor;
423
+ -webkit-mask: var(--base-icon-chevron, #{$base-icon-chevron}) center / 1.2rem no-repeat;
424
+ mask: var(--base-icon-chevron, #{$base-icon-chevron}) center / 1.2rem no-repeat;
425
+
426
+ body.loaded & {
427
+ transition: transform $transition-fast $easing-snappy;
428
+ }
429
+
430
+ // Chevron points right (›) in LTR, left (‹) in RTL
431
+ // When open, rotates 90deg clockwise in LTR, -90deg (counter-clockwise) in RTL.
432
+ // Scoped to the item's OWN chevron (direct toggle child) — a plain descendant
433
+ // selector would also rotate nested items' chevrons whenever an ANCESTOR is
434
+ // open, so a closed sub-group would wrongly show a "down" chevron.
435
+ .pc-sidebar__item--open > .pc-sidebar__toggle > & {
436
+ transform: rotate(90deg);
437
+ }
438
+
439
+ [dir="rtl"] .pc-sidebar__item--open > .pc-sidebar__toggle > & {
440
+ transform: rotate(-90deg);
441
+ }
442
+ }
443
+ }