@keenmate/pure-css 1.1.0 → 1.2.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keenmate/pure-css",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "The Keenmate CSS foundation — --base-* theming contract, PureCSS grid and utility classes. Extracted from pure-admin-core so docs sites, standalone pages and every web/svelte component share one theming layer.",
5
5
  "style": "dist/css/pure-css.css",
6
6
  "exports": {
@@ -24,7 +24,9 @@
24
24
  "src/js/",
25
25
  "README.md",
26
26
  "CHANGELOG.md",
27
- "LICENSE"
27
+ "LICENSE",
28
+ "components.json",
29
+ "COMPONENTS.md"
28
30
  ],
29
31
  "scripts": {
30
32
  "build:bundle": "sass src/scss/pure-css.scss dist/css/pure-css.css --no-source-map --silence-deprecation=import",
@@ -42,7 +44,8 @@
42
44
  "test:e2e:ui": "playwright test --ui",
43
45
  "test:e2e:headed": "playwright test --headed",
44
46
  "test:e2e:install": "playwright install chromium",
45
- "prepublishOnly": "npm run build"
47
+ "prepublishOnly": "npm run build && npm run catalog",
48
+ "catalog": "node scripts/build-catalog.mjs"
46
49
  },
47
50
  "keywords": [
48
51
  "css",
@@ -14,11 +14,18 @@
14
14
  // --ms-accent-color: var(--base-accent-color, #3b82f6);
15
15
  // ============================================================================
16
16
 
17
- // Make variables resolvable when this file is loaded as a @use module
18
- // (e.g. from main.scss). Legacy @import callers (themes) are unaffected —
19
- // Sass places @use'd members into the importing file's global scope, where
20
- // theme-set overrides already live.
21
- @use 'variables/index' as *;
17
+ // Make the $-vocabulary resolvable here.
18
+ //
19
+ // MUST be @import, not `@use 'variables/index' as *`. A theme's whole job is to set
20
+ // $base-* BEFORE pulling this file in (the !default flags then keep those values) --
21
+ // but `@use ... as *` REFUSES to expose a member whose name already exists in the
22
+ // importing global scope, so every theme died on
23
+ // "This module and the new module both define a variable named $base-accent-color".
24
+ // That made the usage documented at the top of this file impossible: it worked only
25
+ // for a theme that overrode nothing. @import shares one global scope and is what the
26
+ // !default mechanism needs. pure-css's own `@use 'base-css-variables' as bcv` entries
27
+ // are unaffected -- a module that @imports still exports its members normally.
28
+ @import 'variables/index';
22
29
 
23
30
  @mixin output-base-css-variables {
24
31
  // === Accent Colors ===
@@ -181,6 +188,10 @@
181
188
  --base-border-radius-sm: #{$base-border-radius-sm};
182
189
  --base-border-radius-md: #{$base-border-radius-md};
183
190
  --base-border-radius-lg: #{$base-border-radius-lg};
191
+ // Checkbox BOX scale — multiplies the box's own size (NOT a transform, which
192
+ // would pixel-snap the mask glyph off-centre). One knob every component with a
193
+ // checkbox reads; pairs with --base-icon-check-size.
194
+ --base-checkbox-scale: #{$base-checkbox-scale};
184
195
 
185
196
  // === Rem base for web components ===
186
197
  --base-rem: #{$base-rem};
@@ -1,64 +1,64 @@
1
- // ── Icon hover affordance (foundation) ────────────────────────────────────────
2
- // See pure-admin/docs/icon-hover-fill-design.md.
3
- //
4
- // Inside an ENABLED interactive control on :hover, icons react — no JS, no SVG
5
- // computation, driven by convention + marker classes the wrapper stamps:
6
- //
7
- // • Masked `.pa-icon` glyphs that declared a filled source on
8
- // `--pa-icon-src-hover` swap their mask to it. NO-OP for outline-only glyphs
9
- // (e.g. Lucide ships no solid), and no marker needed — it's automatic.
10
- // • Font Awesome `<i>` marked `.pc-icon-hover-fill` flips regular → solid via the
11
- // font-weight axis (same codepoint). Needs the `far` regular face present
12
- // (largely FA Pro), else there's nothing lighter to flip from.
13
- // • Outline icons marked `.pc-icon-hover-highlight` recolour to
14
- // `--pc-icon-hover-color` (default: the accent). This is the answer for sets
15
- // with no solid form (Lucide): a real hover cue without a fill.
16
- //
17
- // The behaviour lives here (foundation) so every consumer — pure-admin core,
18
- // keen, svelte, the web components — shares ONE definition. Wrappers only stamp
19
- // the marker classes. `pc-icon-hover-effects` is a mixin so component layers can
20
- // apply the same effects to their own enabled-control selectors (e.g. core on
21
- // `.pa-btn` / `.pa-tabs__item`) without redefining them.
22
- //
23
- // Deliberately NOT here yet: the two-asset outline↔solid toggle for Heroicons /
24
- // Fluent (render both layers, swap visibility) — that needs a wrapper markup
25
- // contract, so it's wired when a wrapper renders both layers.
26
-
27
- @mixin pc-icon-hover-effects {
28
- // Masked icons: swap to the filled mask source if one was declared. Set the
29
- // longhand directly (not --pa-icon-src) so the var(--pa-icon-src) fallback
30
- // stays non-cyclic — reassigning --pa-icon-src to reference itself would
31
- // compute to invalid and wipe the mask to a solid box.
32
- .pa-icon {
33
- -webkit-mask-image: var(--pa-icon-src-hover, var(--pa-icon-src));
34
- mask-image: var(--pa-icon-src-hover, var(--pa-icon-src));
35
- }
36
-
37
- // Font Awesome regular → solid (weight axis). No transition — a font-weight
38
- // swap is instant, matching the mask swap.
39
- .pc-icon-hover-fill {
40
- font-weight: 900;
41
- }
42
-
43
- // Outline-only sets (Lucide): recolour the stroke instead of filling. Works
44
- // because Lucide SVGs are stroke="currentColor".
45
- .pc-icon-hover-highlight {
46
- color: var(--pc-icon-hover-color, var(--pc-accent));
47
- }
48
- }
49
-
50
- // Shell contexts owned by the foundation: sidebar nav links AND submenu toggles,
51
- // so a submenu parent's icon reacts identically to a plain/child link (not just
52
- // to the toggle's own hover colour).
53
- .pc-sidebar__link:not([aria-disabled='true']):not(.is-disabled):hover,
54
- .pc-sidebar__toggle:not([aria-disabled='true']):not(.is-disabled):hover {
55
- @include pc-icon-hover-effects;
56
- }
57
-
58
- // Generic opt-in: put `.pc-icon-hover` on any enabled control/element to get the
59
- // behaviour without depending on a component class (a bare link, a custom
60
- // button, a list row, …). Disabled opt-outs are excluded so only actionable
61
- // controls react.
62
- .pc-icon-hover:not([disabled]):not([aria-disabled='true']):not(.is-disabled):hover {
63
- @include pc-icon-hover-effects;
64
- }
1
+ // ── Icon hover affordance (foundation) ────────────────────────────────────────
2
+ // See pure-admin/docs/icon-hover-fill-design.md.
3
+ //
4
+ // Inside an ENABLED interactive control on :hover, icons react — no JS, no SVG
5
+ // computation, driven by convention + marker classes the wrapper stamps:
6
+ //
7
+ // • Masked `.pa-icon` glyphs that declared a filled source on
8
+ // `--pa-icon-src-hover` swap their mask to it. NO-OP for outline-only glyphs
9
+ // (e.g. Lucide ships no solid), and no marker needed — it's automatic.
10
+ // • Font Awesome `<i>` marked `.pc-icon-hover-fill` flips regular → solid via the
11
+ // font-weight axis (same codepoint). Needs the `far` regular face present
12
+ // (largely FA Pro), else there's nothing lighter to flip from.
13
+ // • Outline icons marked `.pc-icon-hover-highlight` recolour to
14
+ // `--pc-icon-hover-color` (default: the accent). This is the answer for sets
15
+ // with no solid form (Lucide): a real hover cue without a fill.
16
+ //
17
+ // The behaviour lives here (foundation) so every consumer — pure-admin core,
18
+ // keen, svelte, the web components — shares ONE definition. Wrappers only stamp
19
+ // the marker classes. `pc-icon-hover-effects` is a mixin so component layers can
20
+ // apply the same effects to their own enabled-control selectors (e.g. core on
21
+ // `.pa-btn` / `.pa-tabs__item`) without redefining them.
22
+ //
23
+ // Deliberately NOT here yet: the two-asset outline↔solid toggle for Heroicons /
24
+ // Fluent (render both layers, swap visibility) — that needs a wrapper markup
25
+ // contract, so it's wired when a wrapper renders both layers.
26
+
27
+ @mixin pc-icon-hover-effects {
28
+ // Masked icons: swap to the filled mask source if one was declared. Set the
29
+ // longhand directly (not --pa-icon-src) so the var(--pa-icon-src) fallback
30
+ // stays non-cyclic — reassigning --pa-icon-src to reference itself would
31
+ // compute to invalid and wipe the mask to a solid box.
32
+ .pa-icon {
33
+ -webkit-mask-image: var(--pa-icon-src-hover, var(--pa-icon-src));
34
+ mask-image: var(--pa-icon-src-hover, var(--pa-icon-src));
35
+ }
36
+
37
+ // Font Awesome regular → solid (weight axis). No transition — a font-weight
38
+ // swap is instant, matching the mask swap.
39
+ .pc-icon-hover-fill {
40
+ font-weight: 900;
41
+ }
42
+
43
+ // Outline-only sets (Lucide): recolour the stroke instead of filling. Works
44
+ // because Lucide SVGs are stroke="currentColor".
45
+ .pc-icon-hover-highlight {
46
+ color: var(--pc-icon-hover-color, var(--pc-accent));
47
+ }
48
+ }
49
+
50
+ // Shell contexts owned by the foundation: sidebar nav links AND submenu toggles,
51
+ // so a submenu parent's icon reacts identically to a plain/child link (not just
52
+ // to the toggle's own hover colour).
53
+ .pc-sidebar__link:not([aria-disabled='true']):not(.is-disabled):hover,
54
+ .pc-sidebar__toggle:not([aria-disabled='true']):not(.is-disabled):hover {
55
+ @include pc-icon-hover-effects;
56
+ }
57
+
58
+ // Generic opt-in: put `.pc-icon-hover` on any enabled control/element to get the
59
+ // behaviour without depending on a component class (a bare link, a custom
60
+ // button, a list row, …). Disabled opt-outs are excluded so only actionable
61
+ // controls react.
62
+ .pc-icon-hover:not([disabled]):not([aria-disabled='true']):not(.is-disabled):hover {
63
+ @include pc-icon-hover-effects;
64
+ }
@@ -21,110 +21,130 @@
21
21
  --pc-easing-snappy: #{$easing-snappy};
22
22
  }
23
23
 
24
- // Responsive Design - Mobile and Tablet
25
- @media (max-width: $mobile-breakpoint) {
26
- // Mobile: the sidebar is an off-canvas drawer that SLIDES in/out, in sync with
27
- // the profile panel (same $transition-medium / $easing-snappy). It's always
28
- // position:fixed at the drawer size and translated off-canvas at the
29
- // inline-start edge; `.sidebar-visible` slides it to translateX(0). This
30
- // replaces the old snap between width:0 and a fixed overlay, which popped in
31
- // with no animation. Content takes full width in both states because the
32
- // drawer is out of flow (fixed).
33
- .pc-layout__sidebar,
34
- .pc-layout__sidebar--icon-collapse {
35
- position: fixed;
36
- top: $navbar-height;
37
- inset-inline-start: 0; // RTL: flips to right
38
- bottom: 0;
39
- width: $mobile-sidebar-width;
40
- z-index: 1050;
41
- background-color: var(--pc-sidebar-bg, var(--base-page-bg));
42
- opacity: 1;
43
- overflow-y: auto;
44
- overscroll-behavior: contain; // drawer's own scroll doesn't chain to the page
45
- border-inline-end: none; // RTL: flips to left
46
- transform: translateX(-100%); // off-canvas (slid out past the inline-start edge)
47
- visibility: hidden; // inert + unfocusable while closed; flips visible on open
48
-
49
- // Gate the transition on body.loaded like the sidebar links, so the drawer
50
- // doesn't animate from off-canvas on first paint.
51
- body.loaded & {
52
- transition: transform $transition-medium $easing-snappy,
53
- visibility $transition-medium $easing-snappy;
24
+ // Off-canvas drawer overlay — the sidebar SLIDES in/out as a fixed sheet in sync
25
+ // with the profile panel (same $transition-medium / $easing-snappy), translated
26
+ // off-canvas at the inline-start edge; `.sidebar-visible` slides it to
27
+ // translateX(0). Content takes full width in both states because the drawer is
28
+ // out of flow (fixed), so the flex sibling `.pc-layout__content {flex:1}` grows.
29
+ //
30
+ // Shared by two callers via $bodyQualifier:
31
+ // • '' → every sidebar below $mobile-breakpoint (media query).
32
+ // • '.sidebar-overlay' → the opt-in desktop "overlay" sidebar behaviour, at any
33
+ // width (burger opens a temporary floating drawer).
34
+ // The qualifier composes onto `body` so the body.loaded transition gate and the
35
+ // .sidebar-visible open state resolve as SAME-ELEMENT compounds (body.loaded,
36
+ // body.sidebar-visible) — not broken descendant selectors.
37
+ //
38
+ // $drawerWidth differs per caller: mobile uses a near-full-width 90vw sheet;
39
+ // desktop overlay uses the normal $sidebar-width (288px) so the content stays
40
+ // usable behind it and the dismiss zone isn't a sliver.
41
+ @mixin sidebar-drawer-overlay($bodyQualifier: '', $drawerWidth: $mobile-sidebar-width) {
42
+ body#{$bodyQualifier} {
43
+ .pc-layout__sidebar,
44
+ .pc-layout__sidebar--icon-collapse {
45
+ position: fixed;
46
+ top: $navbar-height;
47
+ inset-inline-start: 0; // RTL: flips to right
48
+ bottom: 0;
49
+ width: $drawerWidth;
50
+ z-index: 1050;
51
+ background-color: var(--pc-sidebar-bg, var(--base-page-bg));
52
+ opacity: 1;
53
+ overflow-y: auto;
54
+ overscroll-behavior: contain; // drawer's own scroll doesn't chain to the page
55
+ border-inline-end: none; // RTL: flips to left
56
+ transform: translateX(-100%); // off-canvas (slid out past the inline-start edge)
57
+ visibility: hidden; // inert + unfocusable while closed; flips visible on open
54
58
  }
55
- }
56
59
 
57
- // RTL: the drawer sits at the inline-start (right) edge, so it slides off to
58
- // the right when closed.
59
- [dir="rtl"] .pc-layout__sidebar,
60
- [dir="rtl"] .pc-layout__sidebar--icon-collapse {
61
- transform: translateX(100%);
62
- }
60
+ // RTL: the drawer sits at the inline-start (right) edge, so it slides off to
61
+ // the right when closed.
62
+ [dir="rtl"] & {
63
+ .pc-layout__sidebar,
64
+ .pc-layout__sidebar--icon-collapse {
65
+ transform: translateX(100%);
66
+ }
67
+ }
63
68
 
64
- // icon-collapse mode is a full overlay drawer on mobile — force labels visible.
65
- .pc-layout__sidebar--icon-collapse .pc-sidebar__label {
66
- opacity: 1 !important;
67
- width: auto !important;
68
- overflow: visible !important;
69
- position: static !important;
70
- }
69
+ // icon-collapse mode is a full labelled drawer here — force labels visible.
70
+ .pc-layout__sidebar--icon-collapse .pc-sidebar__label {
71
+ opacity: 1 !important;
72
+ width: auto !important;
73
+ overflow: visible !important;
74
+ position: static !important;
75
+ }
71
76
 
72
- // Backdrop scrim: an always-present fixed pseudo that FADES its opacity, so it
73
- // eases in/out with the drawer (a pseudo that only existed while open couldn't
74
- // fade out). Inert + invisible when closed. On body::before so it's positioned
75
- // against the viewport (not a transformed ancestor) — the toggle class lives
76
- // on body too.
77
- body::before {
78
- content: "";
79
- position: fixed;
80
- top: $navbar-height;
81
- inset-inline: 0; // RTL: covers full width
82
- bottom: 0;
83
- background-color: $mobile-backdrop-bg;
84
- z-index: 1040;
85
- opacity: 0;
86
- visibility: hidden;
87
- pointer-events: none;
88
-
89
- body.loaded & {
90
- transition: opacity $transition-medium $easing-snappy,
91
- visibility $transition-medium $easing-snappy;
77
+ // Backdrop scrim: an always-present fixed pseudo that FADES its opacity, so it
78
+ // eases in/out with the drawer (a pseudo that only existed while open couldn't
79
+ // fade out). Inert + invisible when closed. On the body pseudo so it's
80
+ // positioned against the viewport — the toggle class lives on body too.
81
+ &::before {
82
+ content: "";
83
+ position: fixed;
84
+ top: $navbar-height;
85
+ inset-inline: 0; // RTL: covers full width
86
+ bottom: 0;
87
+ background-color: $mobile-backdrop-bg;
88
+ z-index: 1040;
89
+ opacity: 0;
90
+ visibility: hidden;
91
+ pointer-events: none;
92
92
  }
93
- }
94
93
 
95
- // Mobile: drawer open — slide it in, reveal the scrim, lock background scroll.
96
- .sidebar-visible {
97
- // Standard modal-drawer scroll-lock: while the drawer + scrim are open the
98
- // page behind must not scroll (otherwise dragging the scrim strip scrolls
99
- // the content under the fixed drawer, exposing that the drawer is only
100
- // viewport-high). This covers desktop browsers + Android; iOS Safari ignores
101
- // body{overflow:hidden} for touch scroll, so the mobile toggle also applies
102
- // a position:fixed body lock in JS (demo layout.mustache). See
103
- // docs/mobile-sidebar-overlay.md.
104
- overflow: hidden;
94
+ // Gate the drawer + scrim transitions on body.loaded so neither animates from
95
+ // its closed position on first paint.
96
+ &.loaded {
97
+ .pc-layout__sidebar,
98
+ .pc-layout__sidebar--icon-collapse {
99
+ transition: transform $transition-medium $easing-snappy,
100
+ visibility $transition-medium $easing-snappy;
101
+ }
102
+
103
+ &::before {
104
+ transition: opacity $transition-medium $easing-snappy,
105
+ visibility $transition-medium $easing-snappy;
106
+ }
107
+ }
105
108
 
106
- .pc-layout__sidebar,
107
- .pc-layout__sidebar--icon-collapse {
108
- transform: translateX(0); // slide in (source order beats the RTL closed rule)
109
- visibility: visible;
109
+ // Open — slide the drawer in, reveal the scrim, lock background scroll.
110
+ // Listed LAST so translateX(0) beats both the default and the RTL closed
111
+ // transform at equal specificity (source order wins).
112
+ &.sidebar-visible {
113
+ // Standard modal-drawer scroll-lock: while the drawer + scrim are open the
114
+ // page behind must not scroll (otherwise dragging the scrim strip scrolls
115
+ // the content under the fixed drawer, exposing that the drawer is only
116
+ // viewport-high). This covers desktop browsers + Android; iOS Safari ignores
117
+ // body{overflow:hidden} for touch scroll, so the toggle also applies a
118
+ // position:fixed body lock in JS (demo layout.mustache). See
119
+ // docs/mobile-sidebar-overlay.md.
120
+ overflow: hidden;
121
+
122
+ .pc-layout__sidebar,
123
+ .pc-layout__sidebar--icon-collapse {
124
+ transform: translateX(0);
125
+ visibility: visible;
126
+ }
127
+
128
+ &::before {
129
+ opacity: 1;
130
+ visibility: visible;
131
+ pointer-events: auto; // tap the scrim to dismiss
132
+ }
110
133
  }
111
134
 
112
- &::before {
113
- opacity: 1;
114
- visibility: visible;
115
- pointer-events: auto; // tap the scrim to dismiss
135
+ // Resize handle is meaningless on a fixed overlay drawer (and would ride the
136
+ // overlay edge straddling the backdrop-dismiss tap zone). Touch resize still
137
+ // works in the tablet band above $mobile-breakpoint.
138
+ .pc-sidebar-resize {
139
+ display: none;
116
140
  }
117
141
  }
142
+ }
118
143
 
119
- // Mobile overlay: the sidebar is a fixed 90vw sheet and resize is disabled in
120
- // JS at this width (sidebar-resize.js MOBILE_MAX = $mobile-breakpoint). Hide
121
- // the handle so the coarse-pointer grip pill / edge line don't render as a
122
- // dead affordance riding the overlay's edge (where it also straddles the
123
- // backdrop-dismiss tap zone). Touch resize still works in the tablet band
124
- // above this breakpoint, where the coarse-pointer affordance is kept.
125
- .pc-sidebar-resize {
126
- display: none;
127
- }
144
+ // Responsive Design - Mobile and Tablet
145
+ @media (max-width: $mobile-breakpoint) {
146
+ // Below the mobile breakpoint EVERY sidebar is an off-canvas drawer.
147
+ @include sidebar-drawer-overlay;
128
148
 
129
149
  // Adjust navbar for mobile
130
150
  .pc-navbar__inner {
@@ -146,6 +166,17 @@
146
166
  }
147
167
  }
148
168
 
169
+ // Desktop opt-in: `body.sidebar-overlay` turns the sidebar into a temporary
170
+ // floating drawer — the "overlay" sidebar behaviour. The burger toggles
171
+ // `.sidebar-visible`; a tap on the scrim/outside closes it (demo JS). Gated
172
+ // ABOVE the mobile breakpoint: at/below it the media query already renders the
173
+ // sidebar as a 90vw drawer, so the desktop rules would only fight it (and widen
174
+ // a phone drawer to 288px). Uses the normal $sidebar-width so the drawer floats
175
+ // over content rather than swallowing the viewport.
176
+ @media (min-width: ($mobile-breakpoint + 1)) {
177
+ @include sidebar-drawer-overlay('.sidebar-overlay', $sidebar-width);
178
+ }
179
+
149
180
  @media (max-width: $tablet-breakpoint) and (min-width: $tablet-breakpoint-min) {
150
181
  // Tablet: reduce the sidebar by default, but honour an explicit resize.
151
182
  // The default (un-resized) width caps at $sidebar-width-tablet via min() so