@keenmate/pure-css 1.0.0-rc03 → 1.0.0-rc05

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.
@@ -0,0 +1,419 @@
1
+ @use 'variables/index' as *;
2
+ /* ========================================
3
+ Navbar/Header Elements
4
+ Burger menu, brand, navigation, title, profile button, theme switcher
5
+ ======================================== */
6
+
7
+ // Fit engine (navbar-fit.js): the class it toggles to remove a slot or an
8
+ // inactive step from the row. `!important` beats component display rules (e.g.
9
+ // `.pc-navbar-search { display: flex }`) that would otherwise defeat it; using a
10
+ // class (not inline display) means "show" simply reverts to the element's own
11
+ // stylesheet display. Author non-first steps with this class too, so before JS
12
+ // runs only the largest variant shows (no stacked-variants flash, no-JS-safe).
13
+ .pc-fit-hidden {
14
+ display: none !important;
15
+ }
16
+
17
+ // --- App identity: brand wordmark + version tag -------------------------------
18
+ // The brand container. In markup: <div class="pc-app-header"><h1>…</h1></div>.
19
+ .pc-app-header {
20
+ flex-shrink: 0;
21
+ // No brand padding: the .pc-navbar__start flex `gap` (see _navbar.scss) is the
22
+ // single separator between the burger and the brand.
23
+
24
+ h1 {
25
+ margin: 0;
26
+ font-size: $font-size-lg;
27
+ font-weight: $font-weight-semibold;
28
+ color: var(--pc-navbar-text);
29
+ white-space: nowrap;
30
+ }
31
+
32
+ // Version tag beside the wordmark (e.g. "Pure Admin v2.9.0"). Smaller and
33
+ // muted so it reads as metadata rather than part of the product name.
34
+ &__version {
35
+ margin-inline-start: $spacing-xs;
36
+ font-size: $font-size-xs;
37
+ font-weight: $font-weight-normal;
38
+ color: var(--pc-navbar-text-secondary);
39
+ }
40
+ }
41
+
42
+ // --- Burger (fixed navbar anchor, never degrades) -----------------------------
43
+ .pc-navbar__burger {
44
+ flex-shrink: 0;
45
+ margin-inline-start: $burger-menu-margin-left; // RTL: flips to margin-right
46
+ }
47
+
48
+ // --- Navbar menu: horizontal nav + dropdowns + collapse "More" ----------------
49
+ // Its own block (reused in both the start and end navbar zones).
50
+ .pc-navmenu {
51
+ flex-shrink: 0;
52
+
53
+ // Only style the direct child ul (main navigation)
54
+ > ul {
55
+ list-style: none;
56
+ margin: 0;
57
+ padding: 0;
58
+ display: flex;
59
+ gap: $spacing-md;
60
+
61
+ > li {
62
+ margin: 0; // Disable base margins from _base.scss
63
+ position: relative; // For dropdown positioning
64
+ display: flex;
65
+ align-items: center;
66
+
67
+ > a {
68
+ text-decoration: none;
69
+ color: var(--pc-navbar-text-secondary);
70
+ font-weight: $font-weight-medium;
71
+ padding: $spacing-xs $spacing-sm;
72
+ border-radius: var(--pc-border-radius);
73
+ transition: all $transition-fast $easing-snappy;
74
+ font-size: $font-size-sm;
75
+ display: flex;
76
+ align-items: center;
77
+
78
+ // Hover/active derive their pill background from `currentColor` — a
79
+ // translucent tint of the link's OWN rendered text colour — not the
80
+ // content-area --pc-accent. The navbar sits on --pc-navbar-bg, which
81
+ // several themes set dark or to the accent colour itself (e.g. NATO's
82
+ // navy bar) and then force the nav text light with !important while
83
+ // leaving --pc-navbar-text at its default dark value. A currentColor
84
+ // tint tracks the actual text colour (light there, dark on light
85
+ // bars), so the pill contrasts the bar on every theme; an accent
86
+ // or --pc-navbar-text tint would be dark-on-dark and vanish. Text
87
+ // brightens to --pc-navbar-text, which those themes override to match.
88
+ &:hover {
89
+ color: var(--pc-navbar-text);
90
+ background-color: color-mix(in srgb, currentColor 12%, transparent);
91
+ }
92
+ }
93
+
94
+ // Active top-level item — the current section. Held state = a stronger
95
+ // currentColor pill + semibold, reading clearly above the hover tint on
96
+ // any bar. Modifier goes on the <li> (.pc-navmenu__item--active);
97
+ // scoped through the nav > ul > li chain so it outweighs the base
98
+ // `> li > a` colour.
99
+ &.pc-navmenu__item--active > a {
100
+ &,
101
+ &:hover {
102
+ color: var(--pc-navbar-text);
103
+ background-color: color-mix(in srgb, currentColor 22%, transparent);
104
+ font-weight: $font-weight-semibold;
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ // Dropdown navigation items
111
+ &__item {
112
+ &--has-dropdown {
113
+ position: static; // Position dropdown relative to navbar, not this item
114
+
115
+ // Hover-reveal only where hover exists. On touch it never fires (and
116
+ // sticky-hover would fight the JS), so it's gated out and the `.is-open`
117
+ // class below — toggled by navbar-dropdown.js on tap — drives the menu.
118
+ @media (hover: hover) {
119
+ &:hover > .pc-navmenu__dropdown {
120
+ display: block !important; // Override the !important hide
121
+ }
122
+ }
123
+
124
+ // Touch open state (first-tap-opens; navbar-dropdown.js). Works on all
125
+ // devices, so the same class also drives keyboard/JS opens if wired.
126
+ &.is-open > .pc-navmenu__dropdown {
127
+ display: block !important;
128
+ }
129
+ }
130
+ }
131
+
132
+ &__link {
133
+ display: flex;
134
+ align-items: center;
135
+ gap: $spacing-xs;
136
+ }
137
+
138
+ // Dropdown menus
139
+ &__dropdown {
140
+ display: none !important; // Force hide by default
141
+ position: absolute;
142
+ top: 100%;
143
+ inset-inline-start: 0; // RTL: flips to right: 0
144
+ min-width: 19.2rem;
145
+ background-color: var(--pc-card-bg);
146
+ border: $border-width-base solid var(--pc-border-color);
147
+ border-radius: var(--pc-border-radius);
148
+ box-shadow: $shadow-lg;
149
+ z-index: 1100;
150
+ padding: $spacing-xs 0;
151
+ margin-top: 0;
152
+ list-style: none;
153
+ flex-direction: column; // Override any flex from parent
154
+
155
+ li {
156
+ margin: 0; // Disable base margins from _base.scss
157
+ display: block;
158
+ position: relative;
159
+
160
+ // Create invisible bridge for nested dropdowns (horizontal)
161
+ &.pc-navmenu__item--has-dropdown::after {
162
+ content: '';
163
+ position: absolute;
164
+ top: 0;
165
+ inset-inline-start: 100%; // RTL: flips to right: 100%
166
+ width: 6.4px; // Small invisible bridge
167
+ height: 100%;
168
+ z-index: 1099;
169
+ }
170
+
171
+ // Show nested dropdown on hover (hover devices only — see above).
172
+ @media (hover: hover) {
173
+ &:hover > .pc-navmenu__dropdown {
174
+ display: block !important;
175
+ }
176
+ }
177
+
178
+ // Touch open state for nested submenus (navbar-dropdown.js).
179
+ &.is-open > .pc-navmenu__dropdown {
180
+ display: block !important;
181
+ }
182
+
183
+ a {
184
+ display: block;
185
+ padding: $spacing-sm $spacing-base;
186
+ color: var(--pc-text-color-1);
187
+ border-radius: 0;
188
+ white-space: nowrap;
189
+ text-decoration: none;
190
+ font-weight: $font-weight-medium;
191
+ font-size: $font-size-sm;
192
+
193
+ &:hover {
194
+ background-color: var(--pc-accent-light);
195
+ color: var(--pc-accent);
196
+ }
197
+ }
198
+ }
199
+
200
+ // Level 2 dropdown (nested)
201
+ &--level2 {
202
+ top: 0;
203
+ inset-inline-start: 100%; // RTL: flips to right: 100%
204
+ margin-top: 0;
205
+ margin-inline-start: 0; // RTL: flips to margin-right
206
+ }
207
+ }
208
+
209
+ // On touch, a nested submenu can't be a sideways flyout — `inset-inline-start:
210
+ // 100%` runs it straight off narrow screens (nowhere to fly). So on
211
+ // `(hover: none)` nested dropdowns stack INLINE (accordion) inside the level-1
212
+ // panel: static box, indented items, no shadow/border — the same shape the
213
+ // flattened "More" panel uses, but toggled open per-branch by the `.is-open`
214
+ // class (navbar-dropdown.js) rather than shown all at once. The level-1 panel
215
+ // itself stays an absolute drop below the bar.
216
+ @media (hover: none) {
217
+ &__dropdown &__dropdown {
218
+ position: static;
219
+ inset: auto;
220
+ min-width: 0;
221
+ margin: 0;
222
+ border: none;
223
+ box-shadow: none;
224
+ padding: 0;
225
+
226
+ li a {
227
+ padding-inline-start: $spacing-xl; // indent under the group header
228
+ }
229
+ }
230
+ }
231
+
232
+ // --- Responsive collapse (driven by fit.js, data-pc-fit-nav) -----------
233
+ // A nav opted into progressive collapse must be able to shrink BELOW its
234
+ // content so fit.js's bbox-sum vs clientWidth overflow test becomes truthful.
235
+ // Make the nav shrinkable, and (via :has) let the enclosing start section
236
+ // shrink too — it's flex-shrink:0 by default — so the nav absorbs the squeeze
237
+ // while the burger and brand (also flex-shrink:0) keep their size.
238
+ &[data-pc-fit-nav] {
239
+ flex-shrink: 1;
240
+ min-width: 0;
241
+ // No overflow:hidden here — it would clip the nav items' OWN hover
242
+ // dropdowns (they drop below the bar, and any ancestor overflow:hidden
243
+ // cuts them off). Items stay at natural width (flex-shrink:0) and
244
+ // fit.js detects overflow by summing item widths, so clipping
245
+ // isn't needed to measure it.
246
+
247
+ > ul > li {
248
+ flex-shrink: 0;
249
+ }
250
+ }
251
+
252
+ // The generated "More" trigger. Its panel is parented to <body> (to escape
253
+ // the nav's overflow:hidden) and positioned by fit.js, so there's
254
+ // no inline anchoring to do here — just the trigger's own affordance.
255
+ &__item--more {
256
+ position: relative;
257
+ }
258
+
259
+ &__more-chevron {
260
+ display: inline-block;
261
+ margin-inline-start: $spacing-xs;
262
+ font-size: $font-size-xs;
263
+ transition: transform $transition-fast $easing-snappy;
264
+ }
265
+
266
+ // Chevron points down while the panel is open (click-toggled).
267
+ &__item--more.is-open &__more-chevron {
268
+ transform: rotate(90deg);
269
+ }
270
+
271
+ // The More panel is parented to <body> (escapes the nav's overflow:hidden)
272
+ // and positioned by JS; fit.js toggles --open to reveal it. The
273
+ // compound selector outweighs the base `.pc-navmenu__dropdown { display:none }`.
274
+ &__more-menu {
275
+ z-index: $z-index-navbar + 10; // above the fixed navbar
276
+
277
+ &.pc-navmenu__more-menu--open {
278
+ display: block !important;
279
+ }
280
+ }
281
+
282
+ // Flatten — the answer to "what about multi-level menus" in menu mode.
283
+ // Inside the More panel, a collapsed item that had its own dropdown renders
284
+ // that submenu INLINE (indented, static) instead of a hover flyout, so the
285
+ // whole thing is one scrollable panel with no cascading menus, at any depth.
286
+ &__more-menu {
287
+ .pc-navmenu__dropdown {
288
+ display: block !important; // always shown; overrides the hover-reveal
289
+ position: static;
290
+ min-width: 0;
291
+ border: none;
292
+ box-shadow: none;
293
+ padding: 0;
294
+ margin: 0;
295
+
296
+ li a {
297
+ padding-inline-start: $spacing-xl; // indent under the group header
298
+ }
299
+ }
300
+
301
+ // A parent link that owns a submenu reads as a group header.
302
+ .pc-navmenu__item--has-dropdown > a {
303
+ font-weight: $font-weight-semibold;
304
+ }
305
+
306
+ // No flyout bridge inside the flattened panel.
307
+ .pc-navmenu__item--has-dropdown::after {
308
+ content: none;
309
+ }
310
+ }
311
+ }
312
+
313
+ // The navbar start zone must shrink when it hosts a collapsible menu, so the
314
+ // nav can absorb the squeeze (see the collapse note above). It's flex-shrink:0
315
+ // by default in _navbar.scss; :has() opts it back into shrinking only when a
316
+ // collapsible menu is present, leaving the burger and brand at their size.
317
+ .pc-navbar__start:has(.pc-navmenu[data-pc-fit-nav]) {
318
+ flex-shrink: 1;
319
+ min-width: 0;
320
+ }
321
+
322
+ // --- Page title (the center zone's page header) -------------------------------
323
+ .pc-page-header {
324
+ flex: 1;
325
+ min-width: 0; // Enable ellipsis
326
+ text-align: center;
327
+
328
+ h2 {
329
+ margin: 0;
330
+ font-size: $font-size-lg;
331
+ font-weight: $font-weight-semibold;
332
+ color: var(--pc-navbar-text);
333
+ white-space: nowrap;
334
+ overflow: hidden;
335
+ text-overflow: ellipsis;
336
+ }
337
+ }
338
+
339
+ // --- Profile trigger (navbar end zone) ----------------------------------------
340
+ .pc-navbar__profile-btn {
341
+ flex-shrink: 0;
342
+ }
343
+
344
+ // Burger menu button
345
+ .burger-menu {
346
+ display: flex;
347
+ flex-direction: column;
348
+ justify-content: space-around;
349
+ width: $burger-width;
350
+ height: $burger-height;
351
+ background: transparent;
352
+ border: none;
353
+ cursor: pointer;
354
+ padding: 0;
355
+
356
+ span {
357
+ width: 100%;
358
+ height: $burger-bar-height;
359
+ background-color: var(--pc-navbar-text);
360
+ border-radius: var(--pc-border-radius);
361
+ transition: all $transition-normal $easing-snappy;
362
+ transform-origin: center;
363
+ }
364
+
365
+ &.active {
366
+ span:nth-child(1) {
367
+ transform: rotate(45deg) translate($burger-transform-offset, $burger-transform-offset);
368
+ }
369
+
370
+ span:nth-child(2) {
371
+ opacity: 0;
372
+ }
373
+
374
+ span:nth-child(3) {
375
+ transform: rotate(-45deg) translate($burger-transform-offset, -$burger-transform-offset);
376
+ }
377
+ }
378
+
379
+ // RTL: reverse X translation direction
380
+ [dir="rtl"] &.active {
381
+ span:nth-child(1) {
382
+ transform: rotate(45deg) translate(-$burger-transform-offset, $burger-transform-offset);
383
+ }
384
+
385
+ span:nth-child(3) {
386
+ transform: rotate(-45deg) translate(-$burger-transform-offset, -$burger-transform-offset);
387
+ }
388
+ }
389
+
390
+ &:hover span {
391
+ background-color: var(--pc-accent);
392
+ }
393
+ }
394
+
395
+ // Theme switcher styling
396
+ .theme-switcher {
397
+ display: flex;
398
+ align-items: center;
399
+ gap: $spacing-sm;
400
+
401
+ label {
402
+ color: var(--pc-navbar-text-secondary);
403
+ font-size: $font-size-sm;
404
+ }
405
+
406
+ select {
407
+ padding: $spacing-xs $spacing-sm;
408
+ border: $border-width-base solid var(--pc-border-color);
409
+ border-radius: var(--pc-border-radius);
410
+ background-color: var(--pc-navbar-bg);
411
+ color: var(--pc-navbar-text);
412
+ font-size: $font-size-sm;
413
+
414
+ &:focus {
415
+ outline: none;
416
+ border-color: var(--pc-accent);
417
+ }
418
+ }
419
+ }
@@ -0,0 +1,120 @@
1
+ @use 'variables/index' as *;
2
+ /* ========================================
3
+ Navbar Component
4
+ Fixed navbar and constrained container widths
5
+ ======================================== */
6
+
7
+ // Width the centre slot reserves when it hosts a command-palette search pill
8
+ // (see .pc-navbar__center below). A floor, not a fixed width — the slot still
9
+ // grows via flex:1; this just stops it collapsing to a sliver.
10
+ $navbar-search-reserve-width: 18rem !default;
11
+
12
+ // Navbar - Fixed at top, full width by default
13
+ .pc-navbar {
14
+ position: fixed;
15
+ top: 0;
16
+ inset-inline: 0; // RTL: works for both LTR and RTL
17
+ height: $navbar-height;
18
+ background-color: var(--pc-navbar-bg);
19
+ border-bottom: $border-width-base solid var(--pc-navbar-border-color);
20
+ z-index: $z-index-navbar; // 2000
21
+ display: flex;
22
+ align-items: center;
23
+ justify-content: center; // Center for constrained mode
24
+ }
25
+
26
+ // Constrained navbar - limit navbar width to match container
27
+ // Body class is just a marker, no width styles on body itself
28
+ body.pc-container-sm .pc-navbar {
29
+ max-width: $layout-container-sm;
30
+ inset-inline: 0; // RTL: works for both LTR and RTL
31
+ margin-inline: auto; // RTL: centers in both directions
32
+ }
33
+
34
+ body.pc-container-md .pc-navbar {
35
+ max-width: $layout-container-md;
36
+ inset-inline: 0;
37
+ margin-inline: auto;
38
+ }
39
+
40
+ body.pc-container-lg .pc-navbar {
41
+ max-width: $layout-container-lg;
42
+ inset-inline: 0;
43
+ margin-inline: auto;
44
+ }
45
+
46
+ body.pc-container-xl .pc-navbar {
47
+ max-width: $layout-container-xl;
48
+ inset-inline: 0;
49
+ margin-inline: auto;
50
+ }
51
+
52
+ body.pc-container-2xl .pc-navbar {
53
+ max-width: $layout-container-2xl;
54
+ inset-inline: 0;
55
+ margin-inline: auto;
56
+ }
57
+
58
+ // Navbar inner content. Its direct children are a FIXED burger anchor
59
+ // (.pc-navbar__burger — always visible, never in the fit engine) followed by
60
+ // the three degradable zones (.pc-navbar__start / __center / __end). The burger
61
+ // is a SIBLING of the zones, not inside start, so it stays put while the zones
62
+ // collapse. Its `gap` here is the single burger→start separator (same
63
+ // $spacing-base as .pc-navbar__start's own gap). navbar-fit.js measures these
64
+ // direct children.
65
+ .pc-navbar__inner {
66
+ width: 100%;
67
+ padding: 0 $spacing-base;
68
+ display: flex;
69
+ align-items: center;
70
+ gap: $spacing-base;
71
+ }
72
+
73
+ // Header sections - Start/Center/End layout
74
+ .pc-navbar__start {
75
+ display: flex;
76
+ align-items: center;
77
+ gap: $spacing-base;
78
+ flex-shrink: 0;
79
+ }
80
+
81
+ .pc-navbar__center {
82
+ flex: 1;
83
+ min-width: 0; // Allow shrinking for ellipsis
84
+ display: flex;
85
+ align-items: center;
86
+ justify-content: center;
87
+
88
+ // When the centre slot holds a command-palette search pill, reserve real
89
+ // width for it. The search's min-width lives on a *child* (.pc-navbar-search),
90
+ // which flexbox ignores — so this slot (min-width:0) would collapse to a
91
+ // sliver, letting the pill overflow onto the start/end slots. Worse: that
92
+ // deficit never shrinks the NAV's own slot, so fit.js — which
93
+ // measures nav.clientWidth — never sees it and stops folding while items still
94
+ // overlap. Reserving a floor here forces the squeeze onto the shrinkable
95
+ // .pc-navbar__start (see the :has rule in _navbar-elements.scss) instead, so
96
+ // the nav narrows and the collapse loop fires honestly.
97
+ &:has(.pc-navbar-search) {
98
+ min-width: $navbar-search-reserve-width;
99
+
100
+ // Let the reserved slot's DIRECT flex child shrink, whatever it is. The
101
+ // shrink flexbox needs (min-width:0) has to land on the flex item itself —
102
+ // not on the pill by name. Consumers (svelte-pure-admin etc.) wrap the pill
103
+ // in their own element; a descendant selector shrank the pill but left the
104
+ // wrapper at min-width:auto, so it refused to collapse and overflowed onto
105
+ // the start/end slots. Targeting `> *` is position-agnostic: pill-as-child
106
+ // and pill-in-wrapper both shrink. Guarded by :has(.pc-navbar-search) so it
107
+ // only touches the slot when it actually hosts a search.
108
+ > * {
109
+ min-width: 0;
110
+ }
111
+ }
112
+ }
113
+
114
+ .pc-navbar__end {
115
+ display: flex;
116
+ align-items: center;
117
+ gap: $spacing-base;
118
+ flex-shrink: 0;
119
+ margin-inline-start: auto;
120
+ }