@codenhub/styles 0.3.0 → 0.4.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.
@@ -73,6 +73,12 @@
73
73
  `.btn`. */
74
74
  --ui-clip: none;
75
75
  --ui-clip-tight: none;
76
+ /* Round corners, said explicitly, and the pill and chip corners cleared back
77
+ to `--ui-radius`: a region nested inside `.cyber` would otherwise inherit
78
+ its bevel, its diamond pills, and its square chips. */
79
+ --ui-corner-shape: round;
80
+ --ui-radius-pill: initial;
81
+ --ui-radius-tight: initial;
76
82
  --ui-shadow-inset: ;
77
83
  --ui-shadow-edge: initial;
78
84
  --ui-backdrop: none;
@@ -123,3 +129,68 @@
123
129
  .chunky-tile .btn.icon.p-xs {
124
130
  --ui-shadow-y: 2px;
125
131
  }
132
+
133
+ /* Solo: the same look painted onto the one element carrying the class, for an
134
+ element the package does not style. See docs/internal/solo-utilities.md and
135
+ the note on `.glass-solo`: own knobs and foundation tokens with literal
136
+ fallbacks, never a shared `--ui-*`, `--intent-*`, or `--elevation-color`
137
+ token, unlayered, and not for this package's own components. */
138
+ .chunky-tile-solo {
139
+ --_tile-lift: var(--tile-lift, 4px);
140
+ --_tile-ink: light-dark(var(--color-neutral-400, oklch(70.8% 0 0)), var(--color-neutral-600, oklch(43.9% 0 0)));
141
+ /* With no fill to shade, the bar shades the edge: the colour an unfilled
142
+ tile's bar already is under the aesthetic class. */
143
+ --_tile-bar: color-mix(in oklab, var(--_tile-ink) 72%, rgb(0 0 0));
144
+
145
+ border: 2px solid var(--_tile-ink);
146
+ border-radius: var(--tile-radius, 0.75rem);
147
+ box-shadow: 0 var(--_tile-lift) 0 0 var(--_tile-bar);
148
+ font-family: var(--font-rounded, var(--font-default, ui-sans-serif, system-ui, sans-serif));
149
+ transition:
150
+ box-shadow var(--motion-duration-normal, 200ms) var(--motion-ease, cubic-bezier(0.2, 0, 0, 1)),
151
+ transform var(--motion-duration-normal, 200ms) var(--motion-ease, cubic-bezier(0.2, 0, 0, 1));
152
+ }
153
+
154
+ /* The heavier, tracked label is an action's, as under the aesthetic class. */
155
+ .chunky-tile-solo:is(
156
+ button,
157
+ a[href],
158
+ [role="button"],
159
+ summary,
160
+ input[type="button"],
161
+ input[type="submit"],
162
+ input[type="reset"]
163
+ ) {
164
+ font-weight: 800;
165
+ letter-spacing: 0.04em;
166
+ }
167
+
168
+ /* The press, only on something pressed: an action, not disabled. A solo
169
+ container -- a toast clicked to dismiss -- stays put, the way only `.btn` and
170
+ an opted-in card press under the aesthetic class. */
171
+ .chunky-tile-solo:is(
172
+ button,
173
+ a[href],
174
+ [role="button"],
175
+ summary,
176
+ input[type="button"],
177
+ input[type="submit"],
178
+ input[type="reset"]
179
+ ):active:not(:disabled, [aria-disabled="true"]) {
180
+ box-shadow: 0 0 0 0 var(--_tile-bar);
181
+ transform: translateY(var(--_tile-lift));
182
+ }
183
+
184
+ @media (prefers-reduced-motion: reduce) {
185
+ .chunky-tile-solo:is(
186
+ button,
187
+ a[href],
188
+ [role="button"],
189
+ summary,
190
+ input[type="button"],
191
+ input[type="submit"],
192
+ input[type="reset"]
193
+ ):active:not(:disabled, [aria-disabled="true"]) {
194
+ transform: none;
195
+ }
196
+ }
@@ -0,0 +1,190 @@
1
+ /* Cyber: bevelled corners, a thin bright edge, and a glow in the component's own
2
+ colour. See docs/internal/cyber-aesthetic.md for the decision.
3
+
4
+ Tier 1 only. The bevel is `corner-shape`, not `clip-path`: a clip removes
5
+ every outer shadow, so a clipped chamfer has nothing left to glow, and an
6
+ inset ring cannot follow a diagonal cut the way `.pixel`'s follows its square
7
+ one. `corner-shape` draws the corner itself, and the border, the glow, and the
8
+ focus outline all follow it. Load after the base stylesheet. */
9
+ .cyber {
10
+ /* Knobs, read with a fallback so an ancestor can set them (R8; see
11
+ model.md#rules-for-aesthetics). One cut size for controls and surfaces
12
+ alike, so a button and the card around it read as the same material; the
13
+ two shapes then place that cut on opposite diagonals, so they read as
14
+ different things. A shape knob takes any `border-radius` value -- the
15
+ documented presets are a hexagon, a parallelogram, and a single notch --
16
+ and the cut knob scales the default pair without restating it.
17
+
18
+ The cut is a ceiling, not a size: the default pair caps it at a quarter of
19
+ the element's own box, so a 40px button or a card takes the full cut and a
20
+ 19px icon button about 5px rather than having its corners eaten. Nothing in
21
+ the model scales a corner with a component's size -- `.sm` and `.dense`
22
+ change padding and height, never the radius -- and a radius percentage is
23
+ the one length that already knows the box. Horizontal percentages resolve
24
+ against the width and vertical ones against the height, so on a small wide
25
+ button the cut runs slightly shallower than 45 degrees; at those sizes that
26
+ is not visible. A shape set through the knob is taken as written. */
27
+ --_cyber-cut: var(--cyber-cut, 0.625rem);
28
+ --_cyber-glow: var(--cyber-glow, 8px);
29
+ --_cyber-shape: var(--cyber-shape, min(var(--_cyber-cut), 25%) 0 / min(var(--_cyber-cut), 25%) 0);
30
+ --_cyber-shape-surface: var(--cyber-shape-surface, 0 min(var(--_cyber-cut), 25%) / 0 min(var(--_cyber-cut), 25%));
31
+
32
+ /* The neutral line. A neon hue is the look's signature, but hue is intent's
33
+ (R1), so the default follows the theme the way neobrutalism's and pixel's
34
+ ink does, and `--cyber-ink` lets an application choose a neon neutral
35
+ without the aesthetic imposing one on every palette. A named intent keeps
36
+ its own edge colour. */
37
+ --ui-ink: var(--cyber-ink, light-dark(var(--color-neutral-950), var(--color-neutral-50)));
38
+
39
+ /* Controls cut top-left and bottom-right, surfaces top-right and bottom-left.
40
+ Anything fully round -- a badge, a switch, the progress track, the tooltip
41
+ icon, and through their own full radius a radio and a `.btn.pill` -- cuts
42
+ all the way to points: a diamond, or a hexagon on something wider than it
43
+ is tall. Chips square outright, which is what tells the checkbox from the
44
+ radio beside it. */
45
+ --ui-corner-shape: bevel;
46
+ --ui-radius: var(--_cyber-shape);
47
+ --ui-radius-surface: var(--_cyber-shape-surface);
48
+ --ui-radius-pill: calc(infinity * 1px);
49
+ --ui-radius-tight: 0;
50
+ --ui-border-width: 1px;
51
+ --ui-border-max: 100px;
52
+
53
+ /* The glow: the component's own ink, blurred, with no offset. `box` mixes
54
+ depth as `color-mix(in oklab, var(--intent-border) var(--ui-shadow-ink),
55
+ var(--elevation-color))`, so a transparent depth colour turns 40% ink into
56
+ the intent colour at 40% alpha rather than into a darker shade of it.
57
+ Elevation scales the blur like every shadow, so the glow lights what rests
58
+ above zero -- buttons and cards -- and anything raised, and leaves fields
59
+ and chips crisp. */
60
+ --elevation-color: transparent;
61
+ --ui-shadow-ink: 40%;
62
+ --ui-shadow-x: 0px;
63
+ --ui-shadow-y: 0px;
64
+ --ui-shadow-blur: var(--_cyber-glow);
65
+ --ui-shadow-spread: 0px;
66
+
67
+ /* The glow holds still under the pointer and under a press; the press is the
68
+ base scale, restated because a region nested inside another aesthetic must
69
+ not inherit that one's travel. Reduced motion drops it below. */
70
+ --ui-hover-transform: none;
71
+ --ui-active-shadow-x: 0px;
72
+ --ui-active-shadow-y: 0px;
73
+ --ui-active-transform: scale(0.97);
74
+
75
+ /* Nothing here clips, blurs, thins its fill, or draws its edge as a ring, and
76
+ an aesthetic nested inside one that does has to say so rather than inherit
77
+ it. */
78
+ --ui-clip: none;
79
+ --ui-clip-tight: none;
80
+ --ui-shadow-inset: ;
81
+ --ui-shadow-edge: initial;
82
+ --ui-backdrop: none;
83
+ --ui-bg-alpha: 1;
84
+
85
+ /* Consumer-supplied, the way `.pixel` reads `--font-pixel`. The package ships
86
+ no font binary; the monospace stack is the closest technical face every
87
+ platform already has. */
88
+ font-family: var(--font-cyber, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
89
+ }
90
+
91
+ /* Firefox and Safari do not draw `corner-shape` yet, and there the radius would
92
+ round the corners instead. A rounded cyber element reads as a different
93
+ aesthetic; a square one reads as the same aesthetic simplified. What is fully
94
+ round stays round rather than square: a square radio is a checkbox, and a
95
+ pill is the nearest a diamond gets without a bevel. */
96
+ @supports not (corner-shape: bevel) {
97
+ .cyber {
98
+ --ui-radius: 0;
99
+ --ui-radius-surface: 0;
100
+ }
101
+ }
102
+
103
+ @media (prefers-reduced-motion: reduce) {
104
+ .cyber {
105
+ --ui-active-transform: none;
106
+ }
107
+ }
108
+
109
+ /* Solo: the same look painted onto the one element carrying the class, for an
110
+ element the package does not style. See docs/internal/solo-utilities.md and
111
+ the note on `.glass-solo`: own knobs and foundation tokens with literal
112
+ fallbacks, never a shared `--ui-*`, `--intent-*`, or `--elevation-color`
113
+ token, unlayered, and not for this package's own components. */
114
+ .cyber-solo {
115
+ --_cyber-ink: var(
116
+ --cyber-ink,
117
+ light-dark(var(--color-neutral-950, oklch(14.5% 0 0)), var(--color-neutral-50, oklch(98.5% 0 0)))
118
+ );
119
+
120
+ border: 1px solid var(--_cyber-ink);
121
+ /* The surface shape: the element this exists for is usually a pane. An action
122
+ takes the control shape below. */
123
+ border-radius: var(
124
+ --cyber-shape-surface,
125
+ 0 min(var(--cyber-cut, 0.625rem), 25%) / 0 min(var(--cyber-cut, 0.625rem), 25%)
126
+ );
127
+ corner-shape: bevel;
128
+ /* Always lit: the elevation gate the aesthetic class answers to is `box`'s,
129
+ and a solo element is one the consumer chose to light up. */
130
+ box-shadow: 0 0 var(--cyber-glow, 8px) 0 color-mix(in oklab, var(--_cyber-ink) 40%, transparent);
131
+ font-family: var(--font-cyber, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
132
+ transition: transform var(--motion-duration-normal, 200ms) var(--motion-ease, cubic-bezier(0.2, 0, 0, 1));
133
+ }
134
+
135
+ /* An action takes the control shape, on the opposite diagonal to a pane's. */
136
+ .cyber-solo:is(
137
+ button,
138
+ a[href],
139
+ [role="button"],
140
+ summary,
141
+ input[type="button"],
142
+ input[type="submit"],
143
+ input[type="reset"]
144
+ ) {
145
+ border-radius: var(--cyber-shape, min(var(--cyber-cut, 0.625rem), 25%) 0 / min(var(--cyber-cut, 0.625rem), 25%) 0);
146
+ }
147
+
148
+ @supports not (corner-shape: bevel) {
149
+ .cyber-solo,
150
+ .cyber-solo:is(
151
+ button,
152
+ a[href],
153
+ [role="button"],
154
+ summary,
155
+ input[type="button"],
156
+ input[type="submit"],
157
+ input[type="reset"]
158
+ ) {
159
+ border-radius: 0;
160
+ }
161
+ }
162
+
163
+ /* The press, only on something pressed: an action, not disabled. A solo
164
+ container stays put, the way only `.btn` and an opted-in card press under
165
+ the aesthetic class. */
166
+ .cyber-solo:is(
167
+ button,
168
+ a[href],
169
+ [role="button"],
170
+ summary,
171
+ input[type="button"],
172
+ input[type="submit"],
173
+ input[type="reset"]
174
+ ):active:not(:disabled, [aria-disabled="true"]) {
175
+ transform: scale(0.97);
176
+ }
177
+
178
+ @media (prefers-reduced-motion: reduce) {
179
+ .cyber-solo:is(
180
+ button,
181
+ a[href],
182
+ [role="button"],
183
+ summary,
184
+ input[type="button"],
185
+ input[type="submit"],
186
+ input[type="reset"]
187
+ ):active:not(:disabled, [aria-disabled="true"]) {
188
+ transform: none;
189
+ }
190
+ }
@@ -48,6 +48,12 @@
48
48
 
49
49
  --ui-clip: none;
50
50
  --ui-clip-tight: none;
51
+ /* Round corners, said explicitly, and the pill and chip corners cleared back
52
+ to `--ui-radius`: a region nested inside `.cyber` would otherwise inherit
53
+ its bevel, its diamond pills, and its square chips. */
54
+ --ui-corner-shape: round;
55
+ --ui-radius-pill: initial;
56
+ --ui-radius-tight: initial;
51
57
  /* Nothing here draws its edge as a ring, and an aesthetic nested inside pixel
52
58
  has to say so rather than inherit it. */
53
59
  --ui-shadow-edge: initial;
@@ -64,3 +70,43 @@
64
70
  --ui-surface-ground: var(--color-background);
65
71
  }
66
72
  }
73
+
74
+ /* Solo: the same look painted onto the one element carrying the class, for an
75
+ element the package does not style -- a toast from another library, a plain
76
+ `<div>` on a page that never loaded the base stylesheet. See
77
+ docs/internal/solo-utilities.md.
78
+
79
+ It reads the aesthetic's own knobs and the theme's foundation tokens, each
80
+ with its shipped literal as the fallback, and never a `--ui-*`, `--intent-*`,
81
+ or `--elevation-color` token: those belong to whichever aesthetic an ancestor
82
+ carries, and a solo element has to look the same inside any of them.
83
+ Unlayered and one class deep, so it beats a foreign component's
84
+ zero-specificity rules wherever they load. Not for this package's own
85
+ components, which take the aesthetic class instead. */
86
+ .glass-solo {
87
+ background-color: color-mix(
88
+ in srgb,
89
+ var(--color-background, light-dark(oklch(98.5% 0 0), oklch(14.5% 0 0))) 45%,
90
+ transparent
91
+ );
92
+ border: 1px solid light-dark(rgb(255 255 255 / 0.65), rgb(255 255 255 / 0.22));
93
+ /* The surface corner: the element this exists for is a pane. */
94
+ border-radius: var(--glass-radius-surface, 1rem);
95
+ /* The theme's depth colour as a literal, for the reason above: inside a
96
+ `.chunky-tile` region the token is black. */
97
+ box-shadow:
98
+ 0 1px 2px -1px light-dark(rgb(15 23 42 / 0.08), rgb(0 0 0 / 0.55)),
99
+ 0 18px 40px -16px light-dark(rgb(15 23 42 / 0.08), rgb(0 0 0 / 0.55));
100
+ /* Prefixed first, for the reason `surface` gives: written second, the
101
+ minifier folds the pair into the prefixed one and Firefox loses the blur. */
102
+ -webkit-backdrop-filter: blur(14px) saturate(1.6);
103
+ backdrop-filter: blur(14px) saturate(1.6);
104
+ }
105
+
106
+ @media (prefers-reduced-transparency: reduce) {
107
+ .glass-solo {
108
+ background-color: var(--color-background, light-dark(oklch(98.5% 0 0), oklch(14.5% 0 0)));
109
+ -webkit-backdrop-filter: none;
110
+ backdrop-filter: none;
111
+ }
112
+ }
@@ -7,3 +7,4 @@
7
7
  @import "./glass.css";
8
8
  @import "./pixel.css";
9
9
  @import "./chunky-tile.css";
10
+ @import "./cyber.css";
@@ -43,6 +43,12 @@
43
43
  say so rather than inherit it. */
44
44
  --ui-clip: none;
45
45
  --ui-clip-tight: none;
46
+ /* Round corners, said explicitly, and the pill and chip corners cleared back
47
+ to `--ui-radius`: a region nested inside `.cyber` would otherwise inherit
48
+ its bevel, its diamond pills, and its square chips. */
49
+ --ui-corner-shape: round;
50
+ --ui-radius-pill: initial;
51
+ --ui-radius-tight: initial;
46
52
  --ui-shadow-inset: ;
47
53
  /* The slab is depth cast in ink, not this element's line, so an `.edgeless`
48
54
  button keeps it. Cleared rather than omitted, because a neobrutalist region
@@ -61,3 +67,50 @@
61
67
  .neobrutalism .alert {
62
68
  --_d-elevation: 1;
63
69
  }
70
+
71
+ /* Solo: the same look painted onto the one element carrying the class, for an
72
+ element the package does not style. See docs/internal/solo-utilities.md and
73
+ the note on `.glass-solo`: own knobs and foundation tokens with literal
74
+ fallbacks, never a shared `--ui-*`, `--intent-*`, or `--elevation-color`
75
+ token, unlayered, and not for this package's own components. */
76
+ .neobrutalism-solo {
77
+ --_neo-offset: var(--neo-offset, 4px);
78
+ --_neo-ink: light-dark(var(--color-neutral-950, oklch(14.5% 0 0)), var(--color-neutral-50, oklch(98.5% 0 0)));
79
+
80
+ border: 2px solid var(--_neo-ink);
81
+ border-radius: 0;
82
+ box-shadow: var(--_neo-offset) var(--_neo-offset) 0 0 var(--_neo-ink);
83
+ transition:
84
+ box-shadow var(--motion-duration-normal, 200ms) var(--motion-ease, cubic-bezier(0.2, 0, 0, 1)),
85
+ transform var(--motion-duration-normal, 200ms) var(--motion-ease, cubic-bezier(0.2, 0, 0, 1));
86
+ }
87
+
88
+ /* The press, only on something pressed: an action, not disabled. A solo
89
+ container -- a toast clicked to dismiss -- stays put, the way only `.btn` and
90
+ an opted-in card press under the aesthetic class. */
91
+ .neobrutalism-solo:is(
92
+ button,
93
+ a[href],
94
+ [role="button"],
95
+ summary,
96
+ input[type="button"],
97
+ input[type="submit"],
98
+ input[type="reset"]
99
+ ):active:not(:disabled, [aria-disabled="true"]) {
100
+ box-shadow: 0 0 0 0 var(--_neo-ink);
101
+ transform: translate(var(--_neo-offset), var(--_neo-offset));
102
+ }
103
+
104
+ @media (prefers-reduced-motion: reduce) {
105
+ .neobrutalism-solo:is(
106
+ button,
107
+ a[href],
108
+ [role="button"],
109
+ summary,
110
+ input[type="button"],
111
+ input[type="submit"],
112
+ input[type="reset"]
113
+ ):active:not(:disabled, [aria-disabled="true"]) {
114
+ transform: none;
115
+ }
116
+ }
@@ -78,6 +78,14 @@
78
78
  a grid, and a square is on it. */
79
79
  --ui-clip-tight: none;
80
80
 
81
+ /* Square corners already, but a radius of zero draws no shape; it is
82
+ restated, and the pill and chip corners cleared back to `--ui-radius`, so
83
+ a region nested inside `.cyber` does not inherit its bevel onto anything
84
+ that sets a radius of its own. */
85
+ --ui-corner-shape: round;
86
+ --ui-radius-pill: initial;
87
+ --ui-radius-tight: initial;
88
+
81
89
  --ui-hover-transform: none;
82
90
  --ui-active-transform: none;
83
91
  --ui-backdrop: none;
@@ -86,3 +94,43 @@
86
94
  font to fall back to but a monospace stack. */
87
95
  font-family: var(--font-pixel, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
88
96
  }
97
+
98
+ /* Solo: the same look painted onto the one element carrying the class, for an
99
+ element the package does not style. See docs/internal/solo-utilities.md and
100
+ the note on `.glass-solo`: own knobs and foundation tokens with literal
101
+ fallbacks, never a shared `--ui-*`, `--intent-*`, or `--elevation-color`
102
+ token, unlayered, and not for this package's own components. */
103
+ .pixel-solo {
104
+ --_pixel-unit: var(--pixel-unit, 4px);
105
+ --_pixel-ink: light-dark(var(--color-neutral-950, oklch(14.5% 0 0)), var(--color-neutral-50, oklch(98.5% 0 0)));
106
+
107
+ /* The clip removes a border at the straight sides, so the ring draws the
108
+ edge and the element's own border goes. */
109
+ border: 0;
110
+ border-radius: 0;
111
+ box-shadow: inset 0 0 0 var(--_pixel-unit) var(--_pixel-ink);
112
+ clip-path: polygon(
113
+ 0 var(--_pixel-unit),
114
+ var(--_pixel-unit) var(--_pixel-unit),
115
+ var(--_pixel-unit) 0,
116
+ calc(100% - var(--_pixel-unit)) 0,
117
+ calc(100% - var(--_pixel-unit)) var(--_pixel-unit),
118
+ 100% var(--_pixel-unit),
119
+ 100% calc(100% - var(--_pixel-unit)),
120
+ calc(100% - var(--_pixel-unit)) calc(100% - var(--_pixel-unit)),
121
+ calc(100% - var(--_pixel-unit)) 100%,
122
+ var(--_pixel-unit) 100%,
123
+ var(--_pixel-unit) calc(100% - var(--_pixel-unit)),
124
+ 0 calc(100% - var(--_pixel-unit))
125
+ );
126
+ font-family: var(--font-pixel, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
127
+ }
128
+
129
+ /* The clip takes the focus outline too, so focus is a second inset layer under
130
+ the ring, wider than it so it shows -- shadows paint first-on-top. */
131
+ .pixel-solo:focus-visible {
132
+ box-shadow:
133
+ inset 0 0 0 var(--_pixel-unit) var(--_pixel-ink),
134
+ inset 0 0 0 calc(var(--_pixel-unit) + var(--focus-ring-width, 3px))
135
+ var(--focus-ring, color-mix(in srgb, var(--_pixel-ink) 64%, transparent));
136
+ }
package/dist/tw/box.css CHANGED
@@ -153,6 +153,13 @@
153
153
  color: var(--_fg);
154
154
  border: var(--_width) solid var(--_edge);
155
155
  border-radius: var(--ui-radius, var(--radius-control));
156
+ /* What the radius draws: a round arc by default, a straight cut under an
157
+ aesthetic that bevels (`.cyber`). `round` is the property's initial value,
158
+ so with no aesthetic in scope this is a no-op, and an engine without
159
+ `corner-shape` drops the line at parse time. A component that overrides the
160
+ radius after `@apply box` keeps this, so it bevels by the same amount it
161
+ rounds. See model.md#material-tokens. */
162
+ corner-shape: var(--ui-corner-shape, round);
156
163
  box-shadow: var(--_shadow);
157
164
  clip-path: var(--ui-clip, none);
158
165
  transition:
@@ -91,6 +91,9 @@
91
91
  }
92
92
  }
93
93
 
94
+ /* A pill is a shape the author asked for, so it outranks the aesthetic's
95
+ radius. It still takes the corner shape from `box`, so under a bevelling
96
+ aesthetic the full radius cuts to points at both ends. */
94
97
  &.pill {
95
98
  @apply rounded-full;
96
99
  }
@@ -94,8 +94,10 @@
94
94
  @apply box inline-flex min-h-6 w-fit items-center gap-1 px-2.5 text-xs font-bold;
95
95
 
96
96
  /* Pill by default, but an aesthetic that declares no radius has to mean it, so
97
- the round corner is this component's fallback rather than a hardcoded value. */
98
- border-radius: var(--ui-radius, calc(infinity * 1px));
97
+ the round corner is this component's fallback rather than a hardcoded value.
98
+ An aesthetic that keeps pills distinct from its control corner says so
99
+ through `--ui-radius-pill` first. */
100
+ border-radius: var(--ui-radius-pill, var(--ui-radius, calc(infinity * 1px)));
99
101
  clip-path: var(--ui-clip-tight, var(--ui-clip, none));
100
102
  }
101
103
 
@@ -115,6 +117,8 @@
115
117
  background: linear-gradient(90deg, var(--skeleton-base), var(--skeleton-sheen), var(--skeleton-base));
116
118
  background-size: 200% 100%;
117
119
  border-radius: var(--ui-radius, var(--radius-control));
120
+ /* Not a box, so it reads the corner shape itself, beside the radius. */
121
+ corner-shape: var(--ui-corner-shape, round);
118
122
  }
119
123
 
120
124
  /* The track is a box, the fill is not: `box.css`'s formula composes the frame,
@@ -160,14 +164,18 @@
160
164
  background-color: var(--progress-surface, var(--_progress-surface));
161
165
  border: var(--ui-border-width, var(--border-width)) solid var(--progress-edge, var(--_progress-edge));
162
166
  /* Round by default; an aesthetic that declares no radius squares the track and
163
- its fill together. */
164
- border-radius: var(--ui-radius, calc(infinity * 1px));
167
+ its fill together, and a pill corner reaches both first. */
168
+ border-radius: var(--ui-radius-pill, var(--ui-radius, calc(infinity * 1px)));
169
+ /* Not a box either, and the fill below restates it, so a bevelled track
170
+ carries a bevelled fill rather than a pill inside a cut frame. */
171
+ corner-shape: var(--ui-corner-shape, round);
165
172
 
166
173
  &::after {
167
174
  content: "";
168
175
  @apply absolute start-0 top-0 bottom-0 block h-full transition-[width] duration-(--motion-duration-slow) ease-(--motion-ease);
169
176
 
170
- border-radius: var(--ui-radius, calc(infinity * 1px));
177
+ border-radius: var(--ui-radius-pill, var(--ui-radius, calc(infinity * 1px)));
178
+ corner-shape: var(--ui-corner-shape, round);
171
179
  background-color: var(--progress-color, var(--intent-color));
172
180
  width: var(--progress-value, 0%);
173
181
  z-index: 2;
@@ -487,8 +487,10 @@
487
487
 
488
488
  /* Capped at its own radius: a control radius is sized for a 40px control, and
489
489
  14px of it on a 16px box renders as a circle, which is the one shape a
490
- checkbox must not be. A squaring aesthetic still reaches it. */
491
- border-radius: min(var(--ui-radius, var(--radius-small)), var(--radius-small));
490
+ checkbox must not be. A squaring aesthetic still reaches it, through the
491
+ chip corner first -- `.cyber` squares chips outright, which is also what
492
+ tells its checkbox from its diamond radio. */
493
+ border-radius: min(var(--ui-radius-tight, var(--ui-radius, var(--radius-small))), var(--radius-small));
492
494
 
493
495
  /* Chip geometry: the tight silhouette, and an inset edge no thicker than the
494
496
  line the aesthetic draws elsewhere. A 16px box cannot spend four pixels a
@@ -554,6 +556,10 @@
554
556
  &::after {
555
557
  @apply block size-2 scale-0 rounded-full opacity-0 transition-[transform,opacity] duration-(--motion-duration-normal) ease-(--motion-ease);
556
558
 
559
+ /* The dot is the ring's shape at a smaller size: a diamond inside a diamond
560
+ under a bevelling aesthetic, a circle inside a circle otherwise. */
561
+ corner-shape: var(--ui-corner-shape, round);
562
+
557
563
  background-color: currentcolor;
558
564
  content: "";
559
565
  }
@@ -614,8 +620,10 @@
614
620
 
615
621
  height: var(--switch-height);
616
622
  width: var(--switch-width);
617
- /* Pill by default; a squaring aesthetic reaches the track and the knob together. */
618
- border-radius: var(--ui-radius, calc(infinity * 1px));
623
+ /* Pill by default; a squaring aesthetic reaches the track and the knob
624
+ together, and an aesthetic with a pill corner of its own reaches both
625
+ through that first. */
626
+ border-radius: var(--ui-radius-pill, var(--ui-radius, calc(infinity * 1px)));
619
627
 
620
628
  /* Chip geometry, for the reason given on `checkbox`: a 20px track has no more
621
629
  room for a thick inset edge than a 16px box does. */
@@ -636,7 +644,10 @@
636
644
  tone as the label beside it. Checked, it takes the mark colour like the
637
645
  other two toggles. */
638
646
  background-color: var(--color-text-secondary);
639
- border-radius: var(--ui-radius, calc(infinity * 1px));
647
+ border-radius: var(--ui-radius-pill, var(--ui-radius, calc(infinity * 1px)));
648
+ /* A pseudo-element is not a box: the knob takes the track's corner shape
649
+ itself, so the two stay the same shape. */
650
+ corner-shape: var(--ui-corner-shape, round);
640
651
  content: "";
641
652
  height: var(--switch-knob);
642
653
  width: var(--switch-knob);
@@ -138,6 +138,6 @@
138
138
  --_d-elevation: 0;
139
139
  --_d-ground: var(--intent-subtle);
140
140
 
141
- border-radius: var(--ui-radius, calc(infinity * 1px));
141
+ border-radius: var(--ui-radius-pill, var(--ui-radius, calc(infinity * 1px)));
142
142
  clip-path: none;
143
143
  }
@@ -320,7 +320,7 @@
320
320
 
321
321
  /* Capped at its own radius: a control radius is sized for a control, and a key
322
322
  cap this small would read as a lozenge. A squaring aesthetic still reaches it. */
323
- border-radius: min(var(--ui-radius, var(--radius-small)), var(--radius-small));
323
+ border-radius: min(var(--ui-radius-tight, var(--ui-radius, var(--radius-small))), var(--radius-small));
324
324
  clip-path: var(--ui-clip-tight, var(--ui-clip, none));
325
325
 
326
326
  &:not(.primary, .secondary, .success, .warning, .destructive, .danger, .error, .info, .solid) {
@@ -338,7 +338,7 @@
338
338
  --_d-elevation: 0;
339
339
  --_d-ground: var(--intent-subtle);
340
340
 
341
- border-radius: min(var(--ui-radius, var(--radius-small)), var(--radius-small));
341
+ border-radius: min(var(--ui-radius-tight, var(--ui-radius, var(--radius-small))), var(--radius-small));
342
342
  clip-path: var(--ui-clip-tight, var(--ui-clip, none));
343
343
 
344
344
  &:not(.primary, .secondary, .success, .warning, .destructive, .danger, .error, .info, .solid) {
@@ -0,0 +1,12 @@
1
+ ---
2
+ title: 0.4.0
3
+ date: 2026-09-23
4
+ ---
5
+
6
+ # 0.4.0
7
+
8
+ ## Added
9
+
10
+ - **`.cyber`**, a fifth aesthetic from `@codenhub/styles/aesthetics/cyber` (and `/tw/aesthetics/cyber`, and both aggregates): bevelled corners, a 1px edge, and a glow in each component's own colour. Controls cut top-left and bottom-right, surfaces top-right and bottom-left; what is fully round (a radio, a switch, a pill, a badge) cuts to diamonds and pointed hexagons, and chips square. Knobs: `--cyber-cut`, `--cyber-shape`, `--cyber-shape-surface`, `--cyber-glow`, `--cyber-ink`, and the consumer-supplied `--font-cyber`. The bevel is drawn with `corner-shape`, which only Chromium supports today; Firefox and Safari get square cut corners and round round ones. See [Aesthetics → Cyber](../usage/aesthetics.md#cyber).
11
+ - **A solo class for every aesthetic**: `.glass-solo`, `.neobrutalism-solo`, `.pixel-solo`, `.chunky-tile-solo`, and `.cyber-solo` paint the aesthetic's look onto the one element carrying the class, for an element this package does not style -- a toast from another library, or a `<div>` on a page without the base stylesheet. Each ships in its aesthetic's existing entrypoint and needs nothing else loaded. See [Aesthetics → Solo classes](../usage/aesthetics.md#solo-classes).
12
+ - **Three material tokens**: `--ui-corner-shape` (what a corner draws: `round`, or a `bevel` cut), `--ui-radius-pill` (the corner of what is fully round by default), and `--ui-radius-tight` (the corner of chips, before their `--radius-small` cap). All three default to today's look, so nothing changes unless an aesthetic or your own CSS sets them. See [Customizing → Material tokens](../usage/customizing.md#material-tokens).
@@ -6,6 +6,7 @@ group: Changelog
6
6
 
7
7
  # Changelog
8
8
 
9
+ - [0.4.0](0.4.0.md)
9
10
  - [0.3.0](0.3.0.md)
10
11
  - [0.2.0](0.2.0.md)
11
12
  - [0.1.1](0.1.1.md)
package/docs/concepts.md CHANGED
@@ -12,11 +12,11 @@ Read this before the usage guides. It explains the mental model every component,
12
12
 
13
13
  Every component separates three independent choices, so each can change without a component-specific variant:
14
14
 
15
- | Axis | Question | Classes | Inheritance |
16
- | ------------ | --------------- | ----------------------------------------------------------------------------------------- | ------------ |
17
- | Intent | Which color? | `.primary`, `.secondary`, `.success`, `.warning`, `.destructive`, `.info` | Element only |
18
- | Presentation | How much of it? | `.solid`, `.soft`, `.ghost`, `.edged`, `.edgeless` | Cascades |
19
- | Aesthetic | Made of what? | `.neobrutalism`, `.glass`, `.pixel`, `.chunky-tile` from an optional aesthetic stylesheet | Cascades |
15
+ | Axis | Question | Classes | Inheritance |
16
+ | ------------ | --------------- | --------------------------------------------------------------------------------------------------- | ------------ |
17
+ | Intent | Which color? | `.primary`, `.secondary`, `.success`, `.warning`, `.destructive`, `.info` | Element only |
18
+ | Presentation | How much of it? | `.solid`, `.soft`, `.ghost`, `.edged`, `.edgeless` | Cascades |
19
+ | Aesthetic | Made of what? | `.neobrutalism`, `.glass`, `.pixel`, `.chunky-tile`, `.cyber` from an optional aesthetic stylesheet | Cascades |
20
20
 
21
21
  ```html
22
22
  <section class="soft pixel">
package/docs/index.md CHANGED
@@ -14,7 +14,7 @@ The package ships as ready-to-import compiled CSS, or as Tailwind CSS v4 source
14
14
  - Publishes color, foundation, intent, presentation, and material design tokens as CSS custom properties, themed with `light-dark()`.
15
15
  - Provides a reset, focus-visible handling, and responsive layout/content utilities.
16
16
  - Provides composable classes for buttons, form controls and toggles, alerts, badges, loaders, progress, cards/panels, tables, quotes, code, and tooltips.
17
- - Provides four opt-in aesthetics that restyle the same components by setting material tokens, not by adding component variants.
17
+ - Provides five opt-in aesthetics that restyle the same components by setting material tokens, not by adding component variants, each with a solo class that paints the look onto an element the package does not style.
18
18
  - Provides classless mappings so plain HTML elements pick up base styling without added classes, from a dedicated entrypoint.
19
19
  - Publishes Tailwind CSS v4 source entrypoints alongside the compiled output.
20
20
 
@@ -23,7 +23,7 @@ The package ships as ready-to-import compiled CSS, or as Tailwind CSS v4 source
23
23
  - No JavaScript. There is no runtime, no framework bindings, and no build-time code generation beyond the CSS itself.
24
24
  - No semantic HTML, ARIA, keyboard behavior, focus management, validation, or announcement timing. See [Accessibility](./accessibility.md) for the exact line between what the CSS provides and what an application must still add.
25
25
  - No theme-toggling logic. Theme selection is a class or attribute a consumer applies; wiring a toggle to system preference, storage, or a user setting is outside this package. The `@codenhub/theme` package covers that need for consumers who want it.
26
- - No shipped font binaries. `.pixel` and `.chunky-tile` accept a consumer-supplied font and fall back to a system stack when none is given.
26
+ - No shipped font binaries. `.pixel`, `.chunky-tile`, and `.cyber` accept a consumer-supplied font and fall back to a system stack when none is given.
27
27
  - No compatibility layer for removed tokens or classes. Breaking changes are documented, not silently aliased.
28
28
 
29
29
  ## Next steps