@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.
- package/CHANGELOG.md +195 -0
- package/README.md +69 -23
- package/dist/css/base.css +48 -204
- package/dist/css/grid.css +364 -364
- package/dist/css/pure-css.css +1726 -612
- package/dist/css/reboot.css +5 -5
- package/dist/css/scrollbars.css +9 -9
- package/dist/css/utilities.css +78 -30
- package/package.json +18 -3
- package/src/js/container-breakpoint.js +308 -0
- package/src/js/fit.js +890 -0
- package/src/js/navbar-dropdown.js +164 -0
- package/src/js/pure-css.js +305 -0
- package/src/js/sidebar-resize.js +210 -0
- package/src/scss/_base-css-variables.scss +360 -385
- package/src/scss/_fit-flyout.scss +76 -0
- package/src/scss/_fonts.scss +36 -36
- package/src/scss/_layout-container.scss +168 -0
- package/src/scss/_layout-responsive.scss +181 -0
- package/src/scss/_navbar-elements.scss +419 -0
- package/src/scss/_navbar.scss +120 -0
- package/src/scss/_pa-grid.scss +43 -43
- package/src/scss/_resize-handle.scss +76 -0
- package/src/scss/_sidebar-states.scss +243 -0
- package/src/scss/_sidebar.scss +420 -0
- package/src/scss/base.scss +2 -3
- package/src/scss/grid.scss +1 -1
- package/src/scss/pure-css.scss +20 -5
- package/src/scss/reboot.scss +5 -5
- package/src/scss/scrollbars.scss +10 -10
- package/src/scss/utilities.scss +791 -758
- package/src/scss/variables/_base.scss +232 -232
- package/src/scss/variables/_components.scss +840 -835
- package/src/scss/variables/_layout.scss +62 -62
- package/src/scss/variables/_system.scss +1 -1
- package/src/scss/variables/_typography.scss +37 -37
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure CSS — Navbar Dropdown (touch support for hover dropdowns)
|
|
3
|
+
*
|
|
4
|
+
* The navbar dropdowns (`.pc-navmenu__item--has-dropdown`) open on
|
|
5
|
+
* `:hover` — which never fires on touch. When the dropdown parent is ALSO a
|
|
6
|
+
* real link (e.g. Components → /components/overview), a tap just follows the
|
|
7
|
+
* href and the submenu can never be seen. This module fixes that on touch
|
|
8
|
+
* devices with the "first tap opens, second tap navigates" model:
|
|
9
|
+
*
|
|
10
|
+
* - 1st tap on the parent's own link → open its submenu (navigation
|
|
11
|
+
* suppressed), closing any sibling.
|
|
12
|
+
* - 2nd tap on the SAME (now-open) → follow the link normally.
|
|
13
|
+
* parent
|
|
14
|
+
* - A parent whose href is "#"/empty → pure toggle (never navigates); the
|
|
15
|
+
* (a bare toggler, e.g. "More ›") 2nd tap closes it.
|
|
16
|
+
* - Tap outside / Escape → close all open.
|
|
17
|
+
*
|
|
18
|
+
* It only engages on `(hover: none)` devices — desktop / hover-capable input
|
|
19
|
+
* keeps the plain CSS hover-reveal and normal link navigation, untouched.
|
|
20
|
+
* Nested parents work too (the tapped item and its ancestors stay open).
|
|
21
|
+
*
|
|
22
|
+
* Deliberately skipped:
|
|
23
|
+
* - `.pc-navmenu__more-menu` — the collapse "More" panel flattens submenus to
|
|
24
|
+
* always-visible inline links (fit.js + _navbar-elements.scss),
|
|
25
|
+
* so there's nothing to toggle there.
|
|
26
|
+
* - `.pc-navmenu__item--more` — the collapse "More" trigger owns its own
|
|
27
|
+
* click handling.
|
|
28
|
+
*
|
|
29
|
+
* Pairs with the CSS in `_navbar-elements.scss`, where the hover-reveal is
|
|
30
|
+
* gated behind `@media (hover: hover)` and an `.is-open` class reveal (toggled
|
|
31
|
+
* here) drives the open state on touch.
|
|
32
|
+
*
|
|
33
|
+
* No configuration; auto-inits. Public API (pureCss.components.navDropdown):
|
|
34
|
+
* closeAll() — close every open navbar dropdown
|
|
35
|
+
*/
|
|
36
|
+
(function () {
|
|
37
|
+
'use strict';
|
|
38
|
+
|
|
39
|
+
var OPEN = 'is-open';
|
|
40
|
+
var PARENT = '.pc-navmenu__item--has-dropdown';
|
|
41
|
+
|
|
42
|
+
// Only take over on devices without hover — evaluated per-interaction so a
|
|
43
|
+
// hybrid device (touch + mouse) that reports hover keeps native navigation.
|
|
44
|
+
function noHover() {
|
|
45
|
+
return !!(window.matchMedia && window.matchMedia('(hover: none)').matches);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The parent li's OWN dropdown (direct child), not a descendant submenu.
|
|
49
|
+
function directSubmenu(li) {
|
|
50
|
+
for (var c = li.firstElementChild; c; c = c.nextElementSibling) {
|
|
51
|
+
if (c.classList && c.classList.contains('pc-navmenu__dropdown')) return c;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// The parent li's OWN toggle link (direct child <a>).
|
|
57
|
+
function directLink(li) {
|
|
58
|
+
for (var c = li.firstElementChild; c; c = c.nextElementSibling) {
|
|
59
|
+
if (c.tagName && c.tagName.toLowerCase() === 'a') return c;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function setExpanded(li, on) {
|
|
65
|
+
var a = directLink(li);
|
|
66
|
+
if (a) a.setAttribute('aria-expanded', on ? 'true' : 'false');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function openItem(li) {
|
|
70
|
+
// Close everything that isn't `li` or one of its ancestors, so opening a
|
|
71
|
+
// nested submenu keeps its parents open but folds away sibling branches.
|
|
72
|
+
var open = document.querySelectorAll(PARENT + '.' + OPEN);
|
|
73
|
+
for (var i = 0; i < open.length; i++) {
|
|
74
|
+
if (open[i] === li || open[i].contains(li)) continue;
|
|
75
|
+
open[i].classList.remove(OPEN);
|
|
76
|
+
setExpanded(open[i], false);
|
|
77
|
+
}
|
|
78
|
+
li.classList.add(OPEN);
|
|
79
|
+
setExpanded(li, true);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function closeItem(li) {
|
|
83
|
+
li.classList.remove(OPEN);
|
|
84
|
+
setExpanded(li, false);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function closeAll() {
|
|
88
|
+
var open = document.querySelectorAll(PARENT + '.' + OPEN);
|
|
89
|
+
for (var i = 0; i < open.length; i++) closeItem(open[i]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function onClick(e) {
|
|
93
|
+
var link = e.target.closest ? e.target.closest('a') : null;
|
|
94
|
+
|
|
95
|
+
// A tap that isn't on a dropdown parent's own link: if it's fully
|
|
96
|
+
// outside any dropdown, close everything; otherwise (a submenu child
|
|
97
|
+
// link, or non-link chrome inside a dropdown) leave it to navigate.
|
|
98
|
+
if (!link || link.parentNode == null) {
|
|
99
|
+
if (!e.target.closest || !e.target.closest(PARENT)) closeAll();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
var li = link.parentNode;
|
|
104
|
+
var isToggle = li.classList &&
|
|
105
|
+
li.classList.contains('pc-navmenu__item--has-dropdown') &&
|
|
106
|
+
directLink(li) === link && // the parent's OWN link, not a child
|
|
107
|
+
!link.closest('.pc-navmenu__more-menu') && // not the flattened panel
|
|
108
|
+
!li.classList.contains('pc-navmenu__item--more'); // not the More trigger
|
|
109
|
+
|
|
110
|
+
if (!isToggle) {
|
|
111
|
+
// Clicked a normal link (incl. submenu children): close open menus
|
|
112
|
+
// unless the click is inside one that should stay while navigating.
|
|
113
|
+
if (!link.closest(PARENT)) closeAll();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!directSubmenu(li)) return; // parent flagged has-dropdown but empty
|
|
118
|
+
if (!noHover()) return; // hover device → native nav + CSS hover
|
|
119
|
+
|
|
120
|
+
var href = link.getAttribute('href') || '';
|
|
121
|
+
var togglerOnly = href === '' || href === '#' || href.charAt(href.length - 1) === '#';
|
|
122
|
+
|
|
123
|
+
if (!li.classList.contains(OPEN)) {
|
|
124
|
+
e.preventDefault(); // 1st tap → open, don't navigate
|
|
125
|
+
openItem(li);
|
|
126
|
+
} else if (togglerOnly) {
|
|
127
|
+
e.preventDefault(); // bare toggler → 2nd tap closes
|
|
128
|
+
closeItem(li);
|
|
129
|
+
}
|
|
130
|
+
// else: open + real href → fall through, browser follows the link.
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function onKey(e) {
|
|
134
|
+
if (e.key === 'Escape' || e.keyCode === 27) closeAll();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// A11y: advertise the popup + initial collapsed state on each parent's own
|
|
138
|
+
// link (skipping the flattened panel and the collapse trigger).
|
|
139
|
+
function init(scope) {
|
|
140
|
+
var parents = (scope || document).querySelectorAll(PARENT);
|
|
141
|
+
for (var i = 0; i < parents.length; i++) {
|
|
142
|
+
var li = parents[i];
|
|
143
|
+
if (li.closest('.pc-navmenu__more-menu')) continue;
|
|
144
|
+
if (li.classList.contains('pc-navmenu__item--more')) continue;
|
|
145
|
+
var a = directLink(li);
|
|
146
|
+
if (a && directSubmenu(li)) {
|
|
147
|
+
a.setAttribute('aria-haspopup', 'true');
|
|
148
|
+
if (!a.hasAttribute('aria-expanded')) a.setAttribute('aria-expanded', 'false');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
document.addEventListener('click', onClick);
|
|
154
|
+
document.addEventListener('keydown', onKey);
|
|
155
|
+
|
|
156
|
+
var pa = (window.pureCss = window.pureCss || {});
|
|
157
|
+
(pa.components = pa.components || {}).navDropdown = { closeAll: closeAll, init: init };
|
|
158
|
+
|
|
159
|
+
if (document.readyState === 'loading') {
|
|
160
|
+
document.addEventListener('DOMContentLoaded', function () { init(); });
|
|
161
|
+
} else {
|
|
162
|
+
init();
|
|
163
|
+
}
|
|
164
|
+
})();
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure CSS — shared foundation runtime namespace (window.pureCss)
|
|
3
|
+
*
|
|
4
|
+
* ONE global for the foundation layer. The app-shell engines (fit,
|
|
5
|
+
* navbar-dropdown, sidebar-resize, container-breakpoint) and every downstream
|
|
6
|
+
* framework hang their public handle, cross-component signals, and diagnostics
|
|
7
|
+
* off this object instead of scattering `window.PaThing` globals.
|
|
8
|
+
*
|
|
9
|
+
* This is the FOUNDATION half of what used to be pure-admin.js. It installs the
|
|
10
|
+
* parts that must exist exactly once — the event bus, the debug registry, the
|
|
11
|
+
* viewport source, the OS colour-scheme source, capability-first device
|
|
12
|
+
* classification, the overlay primitives, the open-menu registry, the
|
|
13
|
+
* components registry + initAll, and the FOUNDATION config defaults
|
|
14
|
+
* (mobileBreakpoint, typingDebounceDelay, tabletMinShortSide, fit,
|
|
15
|
+
* containerBreakpoint — the ones the moved engines read).
|
|
16
|
+
*
|
|
17
|
+
* pure-admin.js (the admin facade) ADOPTS these buses by reference when it
|
|
18
|
+
* loads on top, and layers the admin-only config defaults (transition, toast,
|
|
19
|
+
* severity) onto the shared config. When pure-admin is absent (pure-css loaded
|
|
20
|
+
* standalone) this runtime stands on its own.
|
|
21
|
+
*
|
|
22
|
+
* Load-order-safe by construction: this file, and every engine, opens with
|
|
23
|
+
*
|
|
24
|
+
* var pc = (window.pureCss = window.pureCss || {});
|
|
25
|
+
*
|
|
26
|
+
* so the namespace exists no matter which script parses first.
|
|
27
|
+
*
|
|
28
|
+
* Surface:
|
|
29
|
+
* pureCss.events on(topic,fn)->off / once / off / emit(topic,payload)
|
|
30
|
+
* pureCss.viewport { width, height, orientation } live snapshot; emits
|
|
31
|
+
* 'viewport:resize' (rAF-throttled) + 'viewport:orientation'
|
|
32
|
+
* pureCss.colorScheme { mode: 'light'|'dark' } live OS preference; emits
|
|
33
|
+
* 'colorscheme:change'
|
|
34
|
+
* pureCss.device { class: 'mobile'|'tablet'|'desktop', isTouchPrimary }
|
|
35
|
+
* pureCss.overlay lockBodyScroll() -> release; observeKeyboardInset(el)
|
|
36
|
+
* pureCss.config shared UI-behavior baseline (foundation half)
|
|
37
|
+
* pureCss.components per-component handles ({init, initAll, …}); initAll(scope)
|
|
38
|
+
* pureCss.menus open-menu coordination registry (register/closeOthers)
|
|
39
|
+
* pureCss.debug enable/disable/isEnabled/log/aspects
|
|
40
|
+
*/
|
|
41
|
+
(function () {
|
|
42
|
+
'use strict';
|
|
43
|
+
|
|
44
|
+
var pa = (window.pureCss = window.pureCss || {});
|
|
45
|
+
pa.components = pa.components || {};
|
|
46
|
+
|
|
47
|
+
// --- events: a tiny topic bus -----------------------------------------
|
|
48
|
+
if (!pa.events) {
|
|
49
|
+
var topics = {}; // { [topic]: Set<fn> }
|
|
50
|
+
pa.events = {
|
|
51
|
+
on: function (topic, fn) {
|
|
52
|
+
(topics[topic] || (topics[topic] = new Set())).add(fn);
|
|
53
|
+
return function off() { if (topics[topic]) topics[topic].delete(fn); };
|
|
54
|
+
},
|
|
55
|
+
once: function (topic, fn) {
|
|
56
|
+
var off = pa.events.on(topic, function (payload) { off(); fn(payload); });
|
|
57
|
+
return off;
|
|
58
|
+
},
|
|
59
|
+
off: function (topic, fn) { if (topics[topic]) topics[topic].delete(fn); },
|
|
60
|
+
emit: function (topic, payload) {
|
|
61
|
+
if (!topics[topic]) return;
|
|
62
|
+
topics[topic].forEach(function (fn) {
|
|
63
|
+
try { fn(payload); } catch (e) { pa.debug.log('events', 'listener threw for', topic, e); }
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
// Introspection for the future debug console.
|
|
67
|
+
topics: function () { return Object.keys(topics); },
|
|
68
|
+
listenerCount: function (topic) { return topics[topic] ? topics[topic].size : 0; }
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// --- debug: per-aspect toggles + log --------------------------------------
|
|
73
|
+
if (!pa.debug) {
|
|
74
|
+
var enabled = {}; // { [aspect]: true }
|
|
75
|
+
var known = {}; // every aspect name ever seen (for aspects())
|
|
76
|
+
pa.debug = {
|
|
77
|
+
enable: function (aspect) { enabled[aspect] = true; known[aspect] = true; },
|
|
78
|
+
disable: function (aspect) { enabled[aspect] = false; },
|
|
79
|
+
isEnabled: function (aspect) { known[aspect] = true; return enabled[aspect] === true; },
|
|
80
|
+
log: function (aspect) {
|
|
81
|
+
known[aspect] = true;
|
|
82
|
+
if (enabled[aspect] !== true) return;
|
|
83
|
+
var args = Array.prototype.slice.call(arguments, 1);
|
|
84
|
+
console.log.apply(console, ['[pc:' + aspect + ']'].concat(args));
|
|
85
|
+
},
|
|
86
|
+
// { aspect: enabled } for every aspect that's been enabled or probed.
|
|
87
|
+
aspects: function () {
|
|
88
|
+
var out = {};
|
|
89
|
+
Object.keys(known).forEach(function (k) { out[k] = enabled[k] === true; });
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// --- viewport: the single owner of window-level resize/orientation --------
|
|
96
|
+
if (!pa.viewport) {
|
|
97
|
+
var vp = pa.viewport = { width: 0, height: 0, orientation: 'landscape' };
|
|
98
|
+
var raf = null;
|
|
99
|
+
function measure() {
|
|
100
|
+
vp.width = window.innerWidth;
|
|
101
|
+
vp.height = window.innerHeight;
|
|
102
|
+
// CSS-parity: "portrait" == taller-than-wide, device-agnostic.
|
|
103
|
+
vp.orientation = vp.height >= vp.width ? 'portrait' : 'landscape';
|
|
104
|
+
}
|
|
105
|
+
measure();
|
|
106
|
+
window.addEventListener('resize', function () {
|
|
107
|
+
if (raf) return; // coalesce a burst of resizes into one frame
|
|
108
|
+
raf = requestAnimationFrame(function () {
|
|
109
|
+
raf = null;
|
|
110
|
+
measure();
|
|
111
|
+
pa.events.emit('viewport:resize', vp);
|
|
112
|
+
});
|
|
113
|
+
}, { passive: true });
|
|
114
|
+
|
|
115
|
+
// Orientation via matchMedia — well-supported and fires on desktop pivot too.
|
|
116
|
+
var mq = window.matchMedia('(orientation: portrait)');
|
|
117
|
+
var onOrient = function () { measure(); pa.events.emit('viewport:orientation', vp); };
|
|
118
|
+
if (mq.addEventListener) mq.addEventListener('change', onOrient);
|
|
119
|
+
else if (mq.addListener) mq.addListener(onOrient); // Safari <14
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// --- colorScheme: the single owner of the OS light/dark preference --------
|
|
123
|
+
if (!pa.colorScheme) {
|
|
124
|
+
var cs = pa.colorScheme = { mode: 'light' };
|
|
125
|
+
var csmq = window.matchMedia('(prefers-color-scheme: dark)');
|
|
126
|
+
var readScheme = function () { cs.mode = csmq.matches ? 'dark' : 'light'; };
|
|
127
|
+
readScheme();
|
|
128
|
+
var onScheme = function () { readScheme(); pa.events.emit('colorscheme:change', cs); };
|
|
129
|
+
if (csmq.addEventListener) csmq.addEventListener('change', onScheme);
|
|
130
|
+
else if (csmq.addListener) csmq.addListener(onScheme); // Safari <14
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// --- config: the shared UI-behavior baseline (foundation half) ------------
|
|
134
|
+
// The engines that moved here (fit, container-breakpoint) + the shell pieces
|
|
135
|
+
// (device classification, sidebar-resize, search debounce) read these. The
|
|
136
|
+
// admin facade layers transition/toast/severity on top of this same object.
|
|
137
|
+
if (!pa.config) pa.config = {};
|
|
138
|
+
(function initConfig(cfg) {
|
|
139
|
+
// Read a numeric CSS variable off :root (px / ms / unitless → number).
|
|
140
|
+
function readCssNumber(name, fallback) {
|
|
141
|
+
try {
|
|
142
|
+
var raw = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
143
|
+
var n = parseFloat(raw);
|
|
144
|
+
return isNaN(n) ? fallback : n;
|
|
145
|
+
} catch (e) { return fallback; }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Fill only the keys a consumer hasn't already set (shallow).
|
|
149
|
+
function fillDefaults(target, defs) {
|
|
150
|
+
Object.keys(defs).forEach(function (k) {
|
|
151
|
+
if (target[k] === undefined) target[k] = defs[k];
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// mobileBreakpoint (px) — SINGLE-SOURCED from SCSS $mobile-breakpoint via the
|
|
156
|
+
// --pc-mobile-breakpoint CSS var. Honour an explicit consumer override; else
|
|
157
|
+
// derive from CSS; else 768.
|
|
158
|
+
if (cfg.mobileBreakpoint == null) {
|
|
159
|
+
cfg.mobileBreakpoint = readCssNumber('--pc-mobile-breakpoint', 768);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// typingDebounceDelay (ms) — debounce for search / autocomplete / filter inputs.
|
|
163
|
+
if (cfg.typingDebounceDelay == null) {
|
|
164
|
+
cfg.typingDebounceDelay = 300;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// tabletMinShortSide (px) — the phone/tablet boundary applied to the SHORTER
|
|
168
|
+
// viewport side (Material sw600dp), consumed by pureCss.device.
|
|
169
|
+
if (cfg.tabletMinShortSide == null) {
|
|
170
|
+
cfg.tabletMinShortSide = 600;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// fit.* — the Fit engine (fit.js). defaultPriority is the priority a slot
|
|
174
|
+
// falls back to when it declares data-pc-fit but no data-pc-fit-priority.
|
|
175
|
+
cfg.fit = cfg.fit || {};
|
|
176
|
+
fillDefaults(cfg.fit, {
|
|
177
|
+
defaultPriority: 0
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// containerBreakpoint.* — the Container Breakpoint engine. hysteresis is the
|
|
181
|
+
// dead-band applied at every threshold; hiddenClass is the utility class the
|
|
182
|
+
// engine toggles on [data-pc-show] descendants to hide them outside their mode(s).
|
|
183
|
+
cfg.containerBreakpoint = cfg.containerBreakpoint || {};
|
|
184
|
+
fillDefaults(cfg.containerBreakpoint, {
|
|
185
|
+
hysteresis: 1,
|
|
186
|
+
hiddenClass: 'd-none'
|
|
187
|
+
});
|
|
188
|
+
})(pa.config);
|
|
189
|
+
|
|
190
|
+
// --- device: capability-first device classification -----------------------
|
|
191
|
+
// Capability decides FIRST: a non-touch-primary pointer (a real mouse) is
|
|
192
|
+
// ALWAYS 'desktop' at any width. Only touch-primary devices consult the size
|
|
193
|
+
// line: SHORTER viewport side below config.tabletMinShortSide => 'mobile',
|
|
194
|
+
// else 'tablet'. Feature detection (matchMedia), not UA sniffing.
|
|
195
|
+
if (!pa.device) {
|
|
196
|
+
var dev = pa.device = { class: 'desktop', isTouchPrimary: false };
|
|
197
|
+
var coarseMq = window.matchMedia('(pointer: coarse)');
|
|
198
|
+
var hoverMq = window.matchMedia('(hover: hover)');
|
|
199
|
+
var classifyDevice = function () {
|
|
200
|
+
var touchPrimary = coarseMq.matches && !hoverMq.matches;
|
|
201
|
+
dev.isTouchPrimary = touchPrimary;
|
|
202
|
+
if (!touchPrimary) return 'desktop';
|
|
203
|
+
var shortSide = Math.min(window.innerWidth, window.innerHeight);
|
|
204
|
+
var line = (pa.config && pa.config.tabletMinShortSide) || 600;
|
|
205
|
+
return shortSide < line ? 'mobile' : 'tablet';
|
|
206
|
+
};
|
|
207
|
+
var reclassify = function () {
|
|
208
|
+
var next = classifyDevice();
|
|
209
|
+
if (next === dev.class) return;
|
|
210
|
+
dev.class = next;
|
|
211
|
+
pa.events.emit('device:change', dev);
|
|
212
|
+
};
|
|
213
|
+
dev.class = classifyDevice();
|
|
214
|
+
pa.events.on('viewport:resize', reclassify);
|
|
215
|
+
pa.events.on('viewport:orientation', reclassify);
|
|
216
|
+
if (coarseMq.addEventListener) {
|
|
217
|
+
coarseMq.addEventListener('change', reclassify);
|
|
218
|
+
hoverMq.addEventListener('change', reclassify);
|
|
219
|
+
} else if (coarseMq.addListener) { // Safari <14
|
|
220
|
+
coarseMq.addListener(reclassify);
|
|
221
|
+
hoverMq.addListener(reclassify);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// --- overlay: fullscreen-sheet primitives (scroll lock + keyboard inset) ---
|
|
226
|
+
if (!pa.overlay) {
|
|
227
|
+
var lockCount = 0;
|
|
228
|
+
var stashedOverflow = null;
|
|
229
|
+
pa.overlay = {
|
|
230
|
+
// Ref-counted body-scroll lock. Returns an idempotent release fn.
|
|
231
|
+
lockBodyScroll: function () {
|
|
232
|
+
if (lockCount === 0) {
|
|
233
|
+
stashedOverflow = document.body.style.overflow;
|
|
234
|
+
document.body.style.overflow = 'hidden';
|
|
235
|
+
}
|
|
236
|
+
lockCount++;
|
|
237
|
+
var released = false;
|
|
238
|
+
return function () {
|
|
239
|
+
if (released) return;
|
|
240
|
+
released = true;
|
|
241
|
+
lockCount = Math.max(0, lockCount - 1);
|
|
242
|
+
if (lockCount === 0 && stashedOverflow !== null) {
|
|
243
|
+
document.body.style.overflow = stashedOverflow;
|
|
244
|
+
stashedOverflow = null;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
},
|
|
248
|
+
// Pin a fixed fullscreen panel's bottom edge above the soft keyboard by
|
|
249
|
+
// tracking window.visualViewport. Returns a cleanup. No-op where
|
|
250
|
+
// visualViewport is unavailable.
|
|
251
|
+
observeKeyboardInset: function (panel) {
|
|
252
|
+
var vv = window.visualViewport;
|
|
253
|
+
if (!vv || !panel) return function () {};
|
|
254
|
+
var raf = null;
|
|
255
|
+
var apply = function () {
|
|
256
|
+
raf = null;
|
|
257
|
+
panel.style.height = vv.height + 'px';
|
|
258
|
+
panel.style.top = vv.offsetTop + 'px';
|
|
259
|
+
};
|
|
260
|
+
var schedule = function () {
|
|
261
|
+
if (raf) return;
|
|
262
|
+
raf = requestAnimationFrame(apply);
|
|
263
|
+
};
|
|
264
|
+
vv.addEventListener('resize', schedule);
|
|
265
|
+
vv.addEventListener('scroll', schedule);
|
|
266
|
+
schedule();
|
|
267
|
+
return function () {
|
|
268
|
+
vv.removeEventListener('resize', schedule);
|
|
269
|
+
vv.removeEventListener('scroll', schedule);
|
|
270
|
+
panel.style.height = '';
|
|
271
|
+
panel.style.top = '';
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// --- menus: open-menu coordination ----------------------------------------
|
|
278
|
+
if (!pa.menus) {
|
|
279
|
+
pa.menus = {
|
|
280
|
+
closers: [],
|
|
281
|
+
register: function (fn) { this.closers.push(fn); return fn; },
|
|
282
|
+
closeOthers: function (self) {
|
|
283
|
+
this.closers.forEach(function (fn) {
|
|
284
|
+
if (fn !== self) { try { fn(); } catch (e) { /* ignore */ } }
|
|
285
|
+
});
|
|
286
|
+
pa.events.emit('menu:opened', { id: self && self.paMenuId });
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// --- components.initAll: init every registered component under a scope -----
|
|
292
|
+
if (!pa.components.initAll) {
|
|
293
|
+
pa.components.initAll = function (scope) {
|
|
294
|
+
Object.keys(pa.components).forEach(function (name) {
|
|
295
|
+
if (name === 'initAll') return;
|
|
296
|
+
var c = pa.components[name];
|
|
297
|
+
if (!c) return;
|
|
298
|
+
try {
|
|
299
|
+
if (typeof c.initAll === 'function') c.initAll(scope);
|
|
300
|
+
else if (typeof c.init === 'function') c.init(scope);
|
|
301
|
+
} catch (e) { pa.debug.log('components', name + '.initAll threw', e); }
|
|
302
|
+
});
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
})();
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure CSS — Sidebar Resize
|
|
3
|
+
*
|
|
4
|
+
* Drag-to-resize the sidebar. Ships in the pure-css foundation (next to fit.js /
|
|
5
|
+
* navbar-dropdown.js) so consumers don't have to vendor it out of the demo:
|
|
6
|
+
* `--pc-local-sidebar-width` is documented in _sidebar.scss as "modified by JS
|
|
7
|
+
* for resize", and the `.pc-sidebar-resize` handle + `--pc-local-sidebar-min-
|
|
8
|
+
* width` / `--pc-local-sidebar-max-width` bounds are all core CSS — the driving
|
|
9
|
+
* script belongs here too.
|
|
10
|
+
*
|
|
11
|
+
* The drag bounds and rem<->px conversion are READ FROM CSS, not hardcoded:
|
|
12
|
+
* --pc-local-sidebar-min-width (default 18rem) — smallest drag width
|
|
13
|
+
* --pc-local-sidebar-max-width (default 50rem) — largest drag width
|
|
14
|
+
* and rem is converted using the actual root font-size (this framework uses a
|
|
15
|
+
* 10px base, but a consumer/theme that changes it — or the min/max vars — stays
|
|
16
|
+
* consistent with the drag limits automatically). Earlier versions baked in
|
|
17
|
+
* 180/500/288 px and a `/10` rem factor, which silently drifted from these vars.
|
|
18
|
+
*
|
|
19
|
+
* Activate by putting `.pc-layout__sidebar--resizable` on the sidebar (the demo
|
|
20
|
+
* settings panel toggles it). The `.pc-sidebar-resize` handle is created on the
|
|
21
|
+
* fly; double-click it to reset to the stylesheet default.
|
|
22
|
+
*
|
|
23
|
+
* Public API (pureCss.components.sidebarResize): init, reset, setWidth, getWidth.
|
|
24
|
+
* Emits `sidebar:resize` ({width}) on the pureCss event bus at drag end.
|
|
25
|
+
*/
|
|
26
|
+
(function () {
|
|
27
|
+
'use strict';
|
|
28
|
+
|
|
29
|
+
var STORAGE_KEY = 'sidebar-width'; // persisted in px
|
|
30
|
+
|
|
31
|
+
// Below this width the sidebar is an overlay drawer — resize is meaningless.
|
|
32
|
+
// Single-sourced from window.pureCss.config.mobileBreakpoint (which derives
|
|
33
|
+
// from the --pc-mobile-breakpoint CSS var / SCSS $mobile-breakpoint), so this
|
|
34
|
+
// guard and the CSS `.pc-sidebar-resize { display: none }` mobile rule can't
|
|
35
|
+
// drift apart. Falls back to 768 if the namespace isn't present yet.
|
|
36
|
+
function mobileMax() {
|
|
37
|
+
var pa = window.pureCss;
|
|
38
|
+
return (pa && pa.config && pa.config.mobileBreakpoint) || 768;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
var sidebar = null;
|
|
42
|
+
var resizeHandle = null;
|
|
43
|
+
var isResizing = false;
|
|
44
|
+
var startX = 0;
|
|
45
|
+
var startWidth = 0;
|
|
46
|
+
var bounds = { min: 0, max: 0 };
|
|
47
|
+
var initialized = false;
|
|
48
|
+
var rafId = null;
|
|
49
|
+
|
|
50
|
+
// --- CSS-variable helpers ------------------------------------------------
|
|
51
|
+
// Root font-size in px (this framework uses 10px; read it so a changed base
|
|
52
|
+
// doesn't break the px<->rem conversion).
|
|
53
|
+
function rootFontSize() {
|
|
54
|
+
return parseFloat(getComputedStyle(document.documentElement).fontSize) || 10;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Read a length CSS variable off :root and return it in px. Handles rem, px,
|
|
58
|
+
// and bare numbers (treated as px); falls back to fallbackPx if unset.
|
|
59
|
+
function readPxVar(name, fallbackPx) {
|
|
60
|
+
var raw = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
61
|
+
if (!raw) return fallbackPx;
|
|
62
|
+
var n = parseFloat(raw);
|
|
63
|
+
if (isNaN(n)) return fallbackPx;
|
|
64
|
+
if (raw.indexOf('rem') !== -1) return n * rootFontSize();
|
|
65
|
+
return n; // px or unitless
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Re-read the drag bounds from CSS (called at drag start, so a runtime theme
|
|
69
|
+
// switch that changes the min/max is honoured without re-init).
|
|
70
|
+
function refreshBounds() {
|
|
71
|
+
bounds.min = readPxVar('--pc-local-sidebar-min-width', 180);
|
|
72
|
+
bounds.max = readPxVar('--pc-local-sidebar-max-width', 500);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// --- init ----------------------------------------------------------------
|
|
76
|
+
function init() {
|
|
77
|
+
if (initialized && resizeHandle && resizeHandle.parentNode) return;
|
|
78
|
+
|
|
79
|
+
sidebar = document.querySelector('.pc-layout__sidebar--resizable');
|
|
80
|
+
if (!sidebar) return;
|
|
81
|
+
|
|
82
|
+
var existingHandle = sidebar.querySelector('.pc-sidebar-resize');
|
|
83
|
+
if (existingHandle) {
|
|
84
|
+
resizeHandle = existingHandle;
|
|
85
|
+
initialized = true;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
resizeHandle = document.createElement('div');
|
|
90
|
+
resizeHandle.className = 'pc-sidebar-resize';
|
|
91
|
+
resizeHandle.setAttribute('title', 'Drag to resize sidebar');
|
|
92
|
+
sidebar.appendChild(resizeHandle);
|
|
93
|
+
|
|
94
|
+
loadSavedWidth();
|
|
95
|
+
|
|
96
|
+
resizeHandle.addEventListener('mousedown', startResize);
|
|
97
|
+
resizeHandle.addEventListener('touchstart', startResize, { passive: false });
|
|
98
|
+
resizeHandle.addEventListener('dblclick', resetWidth); // reset to default
|
|
99
|
+
|
|
100
|
+
initialized = true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function loadSavedWidth() {
|
|
104
|
+
var saved = localStorage.getItem(STORAGE_KEY);
|
|
105
|
+
if (!saved) return;
|
|
106
|
+
var width = parseInt(saved, 10);
|
|
107
|
+
if (isNaN(width)) return;
|
|
108
|
+
refreshBounds();
|
|
109
|
+
if (width >= bounds.min && width <= bounds.max) setSidebarWidth(width);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Write width (px) as rem on :root, using the real root font-size.
|
|
113
|
+
// Flag body so the tablet-band (769–1024px) width rule honours this explicit
|
|
114
|
+
// width instead of the min() cap — without the flag the cap clamps every
|
|
115
|
+
// draggable width (drag floor 18rem > 16rem cap) and the handle looks dead.
|
|
116
|
+
function setSidebarWidth(width) {
|
|
117
|
+
var rem = width / rootFontSize();
|
|
118
|
+
document.documentElement.style.setProperty('--pc-local-sidebar-width', rem + 'rem');
|
|
119
|
+
document.body.classList.add('pc-sidebar-resized');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getCurrentWidth() {
|
|
123
|
+
return sidebar ? sidebar.offsetWidth : readPxVar('--pc-local-sidebar-width', 288);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function startResize(e) {
|
|
127
|
+
// Skip when the sidebar is collapsed or on mobile (overlay mode).
|
|
128
|
+
if (document.body.classList.contains('sidebar-hidden') ||
|
|
129
|
+
window.innerWidth <= mobileMax()) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
isResizing = true;
|
|
135
|
+
refreshBounds(); // pick up any theme-driven min/max change
|
|
136
|
+
|
|
137
|
+
startX = e.type === 'touchstart' ? e.touches[0].clientX : e.clientX;
|
|
138
|
+
startWidth = getCurrentWidth();
|
|
139
|
+
|
|
140
|
+
resizeHandle.classList.add('pc-sidebar-resize--active');
|
|
141
|
+
document.body.classList.add('pc-sidebar-resizing');
|
|
142
|
+
|
|
143
|
+
document.addEventListener('mousemove', doResize);
|
|
144
|
+
document.addEventListener('mouseup', stopResize);
|
|
145
|
+
document.addEventListener('touchmove', doResize, { passive: false });
|
|
146
|
+
document.addEventListener('touchend', stopResize);
|
|
147
|
+
document.addEventListener('touchcancel', stopResize); // clean up if the browser cancels the touch
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Throttled with requestAnimationFrame.
|
|
151
|
+
function doResize(e) {
|
|
152
|
+
if (!isResizing) return;
|
|
153
|
+
e.preventDefault();
|
|
154
|
+
|
|
155
|
+
var currentX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX;
|
|
156
|
+
if (rafId) cancelAnimationFrame(rafId);
|
|
157
|
+
rafId = requestAnimationFrame(function () {
|
|
158
|
+
var diff = currentX - startX;
|
|
159
|
+
var newWidth = Math.max(bounds.min, Math.min(bounds.max, startWidth + diff));
|
|
160
|
+
setSidebarWidth(newWidth);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function stopResize() {
|
|
165
|
+
if (!isResizing) return;
|
|
166
|
+
isResizing = false;
|
|
167
|
+
|
|
168
|
+
if (rafId) {
|
|
169
|
+
cancelAnimationFrame(rafId);
|
|
170
|
+
rafId = null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
resizeHandle.classList.remove('pc-sidebar-resize--active');
|
|
174
|
+
document.body.classList.remove('pc-sidebar-resizing');
|
|
175
|
+
|
|
176
|
+
document.removeEventListener('mousemove', doResize);
|
|
177
|
+
document.removeEventListener('mouseup', stopResize);
|
|
178
|
+
document.removeEventListener('touchmove', doResize);
|
|
179
|
+
document.removeEventListener('touchend', stopResize);
|
|
180
|
+
document.removeEventListener('touchcancel', stopResize);
|
|
181
|
+
|
|
182
|
+
var finalWidth = getCurrentWidth();
|
|
183
|
+
localStorage.setItem(STORAGE_KEY, finalWidth.toString());
|
|
184
|
+
if (window.pureCss && window.pureCss.events) {
|
|
185
|
+
window.pureCss.events.emit('sidebar:resize', { width: finalWidth });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Reset by dropping the inline override so the stylesheet default applies
|
|
190
|
+
// (the base $sidebar-width, or the tablet cap in the 769–1024px band).
|
|
191
|
+
function resetWidth() {
|
|
192
|
+
document.documentElement.style.removeProperty('--pc-local-sidebar-width');
|
|
193
|
+
document.body.classList.remove('pc-sidebar-resized');
|
|
194
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (document.readyState === 'loading') {
|
|
198
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
199
|
+
} else {
|
|
200
|
+
init();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
var pa = (window.pureCss = window.pureCss || {});
|
|
204
|
+
(pa.components = pa.components || {}).sidebarResize = {
|
|
205
|
+
init: init,
|
|
206
|
+
reset: resetWidth,
|
|
207
|
+
setWidth: setSidebarWidth,
|
|
208
|
+
getWidth: getCurrentWidth
|
|
209
|
+
};
|
|
210
|
+
})();
|