@uncinq/css-components 1.27.3 → 1.28.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/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  <img width="1280" height="640" alt="share-css-components" src="https://github.com/user-attachments/assets/3d74df8a-e7bf-4ba5-8529-99cfef0bc176" />
6
6
 
7
- 30 components and 1 utility, written as plain CSS, reading their values from [@uncinq/component-tokens](https://github.com/uncinq/component-tokens) and adding none of their own.
7
+ 31 components and 1 utility, written as plain CSS, reading their values from [@uncinq/component-tokens](https://github.com/uncinq/component-tokens) and adding none of their own.
8
8
 
9
9
  ## Where this sits
10
10
 
@@ -55,7 +55,7 @@ The failure is silent. No error is raised, the page simply stays at its mobile v
55
55
  | --- | --- |
56
56
  | Buttons | `.btn` with colour, size and style variants, plus `btn-close`, `btn-menu`, `btn-search`, `btn-filter`, `btn-share`, `btn-toc`, `btn-toggle-video` |
57
57
  | Overlays | `modal`, `drawer`, `dropdown`, and the shared panel skeleton |
58
- | Content | `alert`, `badge`, `banner`, `card`, `items`, `list`, `media`, `push`, `surtitle` |
58
+ | Content | `alert`, `badge`, `banner`, `card`, `items`, `list`, `media`, `push`, `rating`, `surtitle` |
59
59
  | Navigation | `nav`, `nav-title`, `nav-accessibility`, `breadcrumb`, `pagination` |
60
60
  | Embeds | `embed`, `video`, `map` |
61
61
  | Forms | `form`, `form-check` and the layout helpers |
@@ -85,7 +85,7 @@ It is also versioned with the code in [`docs/`](docs/), one page per component,
85
85
  - [Cascade layers](docs/cascade-layers.md) — the two layers, and the `:where()` convention
86
86
  - [Buttons](docs/buttons/) — [`.btn`](docs/buttons/btn.md) and the seven specialised buttons
87
87
  - [Overlays](docs/overlays/) — [the panel skeleton](docs/overlays/panel.md), [`.modal`](docs/overlays/modal.md), [`.drawer`](docs/overlays/drawer.md), [`.dropdown`](docs/overlays/dropdown.md)
88
- - [Content](docs/content/) — [`.alert`](docs/content/alert.md), [`.badge`](docs/content/badge.md), [`.banner`](docs/content/banner.md), [`.card`](docs/content/card.md), [`.items`](docs/content/items.md), [`.list`](docs/content/list.md), [`.media`](docs/content/media.md), [`.surtitle`](docs/content/surtitle.md)
88
+ - [Content](docs/content/) — [`.alert`](docs/content/alert.md), [`.badge`](docs/content/badge.md), [`.banner`](docs/content/banner.md), [`.card`](docs/content/card.md), [`.items`](docs/content/items.md), [`.list`](docs/content/list.md), [`.media`](docs/content/media.md), [`.rating`](docs/content/rating.md), [`.surtitle`](docs/content/surtitle.md)
89
89
  - [Navigation](docs/navigation/) — [`.nav`](docs/navigation/nav.md), [`.nav-title`](docs/navigation/nav-title.md), [skip links](docs/navigation/nav-accessibility.md), [breadcrumb](docs/navigation/breadcrumb.md), [pagination](docs/navigation/pagination.md)
90
90
  - [Embeds](docs/embeds/) — [`.embed`](docs/embeds/embed.md), [`.video`](docs/embeds/video.md), [`.map`](docs/embeds/map.md)
91
91
  - [Forms](docs/forms/) — [`.form`](docs/forms/form.md), [`.form-check`](docs/forms/form-check.md)
@@ -1,4 +1,17 @@
1
1
  /* components/form.css */
2
+ /*
3
+ * .form — responsive grid of one or two columns, and its layout helpers.
4
+ *
5
+ * Structure:
6
+ * form.form[.is-submitting|.is-success|.has-error]
7
+ * p.required (meaning of the asterisk, before the first field: RGAA 11.10)
8
+ * div[.full]* (fields)
9
+ * .alert[hidden] (answer of an async submission)
10
+ * div.actions (submit, legal)
11
+ *
12
+ * States, set by the script that submits the form asynchronously:
13
+ * .is-submitting, .is-success (only the .alert remains), .has-error
14
+ */
2
15
  @layer components {
3
16
  .form {
4
17
  display: grid;
@@ -14,10 +27,18 @@
14
27
  }
15
28
 
16
29
  .actions,
17
- .full {
30
+ .alert,
31
+ .full,
32
+ .required {
18
33
  grid-column: 1 / -1;
19
34
  }
20
-
35
+
36
+ /* Aligned with the labels, whatever the alignment of the container */
37
+ .required {
38
+ margin-block: 0;
39
+ text-align: start;
40
+ }
41
+
21
42
  .actions {
22
43
  align-items: start;
23
44
  display: flex;
@@ -28,6 +49,15 @@
28
49
  }
29
50
  }
30
51
 
52
+ &.is-submitting {
53
+ cursor: progress;
54
+ }
55
+
56
+ /* Once sent, only the answer remains (direct children only, so that the
57
+ content of the .alert stays) */
58
+ &.is-success > :not(.alert) {
59
+ display: none;
60
+ }
31
61
  }
32
62
 
33
63
  .form-check {
@@ -0,0 +1,41 @@
1
+ /* components/rating.css */
2
+ /*
3
+ * .rating — star rating out of 5, fractional values included (4.8).
4
+ *
5
+ * Structure:
6
+ * div.rating
7
+ * span.rating-star (×5, style="--rating-fill: 0–100%")
8
+ * .icon (the theme's star glyph, painted as a mask)
9
+ * span.visually-hidden (e.g. "Rating: 4.8 out of 5")
10
+ *
11
+ * The icon is a mask, so its background is what shows through the glyph.
12
+ * A hard-stop gradient paints each star in the active colour up to its
13
+ * --rating-fill and in the empty colour after it.
14
+ */
15
+ @layer components {
16
+ .rating {
17
+ --icon-size: var(--rating-size);
18
+
19
+ align-items: center;
20
+ display: flex;
21
+ gap: var(--rating-gap);
22
+ justify-content: var(--rating-justify-content);
23
+ }
24
+
25
+ .rating-star {
26
+ display: flex;
27
+
28
+ .icon {
29
+ background: linear-gradient(
30
+ to right,
31
+ var(--rating-color-active) var(--rating-fill, 0%),
32
+ var(--rating-color-empty) var(--rating-fill, 0%)
33
+ );
34
+
35
+ /* RTL: stars fill from the right */
36
+ &:dir(rtl) {
37
+ scale: -1 1;
38
+ }
39
+ }
40
+ }
41
+ }
package/css/index.css CHANGED
@@ -27,6 +27,7 @@
27
27
  @import 'components/pagination.css';
28
28
  @import 'components/panel.css';
29
29
  @import 'components/push.css';
30
+ @import 'components/rating.css';
30
31
  @import 'components/surtitle.css';
31
32
  @import 'components/video.css';
32
33
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  isIndex: false
3
3
  title: Content
4
- description: Alerts, badges, banners, cards, the items grid, labelled lists, media objects, pushes and surtitles.
4
+ description: Alerts, badges, banners, cards, the items grid, labelled lists, media objects, pushes, star ratings and surtitles.
5
5
  weight: 5
6
6
  icon: card-text
7
7
  ---
@@ -0,0 +1,68 @@
1
+ ---
2
+ isIndex: false
3
+ title: Rating
4
+ description: A star rating out of 5 with fractional values, how each star is filled through its icon mask, and what the markup has to provide.
5
+ weight: 10
6
+ icon: star-half
7
+ ---
8
+
9
+ A star rating out of 5, fractional values included: 4.8 shows four full stars and one filled to 80%.
10
+
11
+ ```html
12
+ <div class="rating">
13
+ <span class="rating-star" style="--rating-fill: 100%"><i class="icon icon-star" aria-hidden="true"></i></span>
14
+ <span class="rating-star" style="--rating-fill: 100%"><i class="icon icon-star" aria-hidden="true"></i></span>
15
+ <span class="rating-star" style="--rating-fill: 100%"><i class="icon icon-star" aria-hidden="true"></i></span>
16
+ <span class="rating-star" style="--rating-fill: 100%"><i class="icon icon-star" aria-hidden="true"></i></span>
17
+ <span class="rating-star" style="--rating-fill: 80%"><i class="icon icon-star" aria-hidden="true"></i></span>
18
+ <span class="visually-hidden">Rating: 4.8 out of 5</span>
19
+ </div>
20
+ ```
21
+
22
+ | Child | Role |
23
+ | --- | --- |
24
+ | `.rating-star` | One star, carrying its own fill in `--rating-fill`, from `0%` to `100%` |
25
+ | `.icon` | The star glyph, rendered by the theme |
26
+ | `.visually-hidden` | The value, read out by screen readers |
27
+
28
+ The fill is computed where the markup is written, not here: star *n* takes `(rating − n + 1) × 100%`, clamped between `0%` and `100%`. A star with no `--rating-fill` is empty.
29
+
30
+ ## The fill goes through the mask
31
+
32
+ This package ships no star. The `.icon` is the theme's, and the rating expects it to be a **mask**: an element whose `background` shows through the glyph, as the Hugolify icons render.
33
+
34
+ ```css
35
+ .rating-star .icon {
36
+ background: linear-gradient(
37
+ to right,
38
+ var(--rating-color-active) var(--rating-fill, 0%),
39
+ var(--rating-color-empty) var(--rating-fill, 0%)
40
+ );
41
+ }
42
+ ```
43
+
44
+ Both colour stops sit at the same position, so the gradient is a hard edge, not a blend: the glyph is in the active colour up to its fill and in the empty colour after it. With an outline icon set, such as Lucide, that is the outline which changes colour, not the inside of the star.
45
+
46
+ An icon inlined as an `<svg>` ignores the background and is not filled.
47
+
48
+ Under `:dir(rtl)` the glyph is mirrored, so stars fill from the right.
49
+
50
+ ## Size and alignment
51
+
52
+ The rating sets `--icon-size` from `--rating-size`, so the stars follow the rating rather than the font size around them.
53
+
54
+ It is a flex row aligned to the start. `--rating-justify-content` moves it, which is how a centred context, a quote for instance, centres its stars without a modifier:
55
+
56
+ ```css
57
+ .quote .rating {
58
+ --rating-justify-content: center;
59
+ }
60
+ ```
61
+
62
+ `--rating-gap` is `0` by default: the star glyph carries its own inner margin, and a gap would only widen the row.
63
+
64
+ ## Accessibility
65
+
66
+ The stars carry no meaning for assistive technology: each `.icon` is `aria-hidden`, and the value is given as text in a `.visually-hidden` span, in words rather than as a fraction.
67
+
68
+ The empty colour defaults to `--color-text-muted` rather than a border colour, so an empty star stays visible on a muted background such as a card.
@@ -2,7 +2,7 @@
2
2
  isIndex: false
3
3
  title: Surtitle
4
4
  description: An eyebrow label above a heading, tightened automatically when it sits inside a card.
5
- weight: 10
5
+ weight: 11
6
6
  icon: type-h2
7
7
  ---
8
8
 
@@ -55,15 +55,38 @@ Change the floor with `--form-col-min-width`. Raising it past 50% forces a singl
55
55
  | `.full` | Spans both columns, `grid-column: 1 / -1` |
56
56
  | `.actions` | Spans both columns and stacks its children vertically |
57
57
  | `.help` | Help text under a field |
58
+ | `.required` | Spans both columns, aligned to the start, no block margin |
59
+ | `.alert` | Spans both columns |
58
60
 
59
61
  `.actions` is a column flex container aligned to the start, with `--gap` between its children and their block margins flattened. So a submit button, a consent line and a legal note stack in the order written without fighting each other's margins.
60
62
 
61
63
  `.help` reads `--help-*` for colour, size, weight, margin and alignment. Pair it with `aria-describedby`, or it is seen and never announced.
62
64
 
65
+ `.required` is the line explaining the asterisk of the required fields. Put it before the first field: the RGAA wants the meaning of a non-textual indication such as `*` given before its first use, visually and in the source order. It stays aligned with the labels even in a centred container.
66
+
67
+ ## Async submission
68
+
69
+ A script that submits the form without leaving the page shows its answer in an `.alert` placed among the fields, `hidden` until then, and sets these states on the form:
70
+
71
+ | State | Effect |
72
+ | --- | --- |
73
+ | `.is-submitting` | `cursor: progress` while the request runs |
74
+ | `.is-success` | Hides every direct child but the `.alert`, so only the answer remains |
75
+ | `.has-error` | No rule: the fields stay, the `.alert` (e.g. `.alert-danger`) carries the error |
76
+
77
+ ```html
78
+ <form class="form is-success">
79
+ <p class="required">* Required fields</p>
80
+ <div>…</div>
81
+ <p class="alert alert-success" role="status" tabindex="-1">Thank you!</p>
82
+ <div class="actions">…</div>
83
+ </form>
84
+ ```
85
+
63
86
  ## Field spacing
64
87
 
65
88
  Both axes use `--gap`, the same token the grid uses for its column calculation. That is why the two-column cap subtracts `var(--gap) / 2`: the track width and the gutter come from one value, so changing it cannot desynchronise them.
66
89
 
67
90
  ## Controls are not styled here
68
91
 
69
- Inputs, selects, textareas and their focus and invalid states come from [css-base](../../../css-base/base/form/), applied to the native elements. This file adds layout and nothing else, which is why a form built without `.form` still looks right, it is simply stacked.
92
+ Inputs, selects, textareas and their focus and invalid states come from [css-base](../../../css-base/base/form/), applied to the native elements. This file adds layout and the submission states, nothing else, which is why a form built without `.form` still looks right, it is simply stacked.
package/docs/overview.md CHANGED
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  isIndex: false
3
3
  title: Overview
4
- description: The 30 components and 1 utility, how to install them, the import order inside the package, and what it deliberately leaves out.
4
+ description: The 31 components and 1 utility, how to install them, the import order inside the package, and what it deliberately leaves out.
5
5
  weight: 1
6
6
  icon: book
7
7
  ---
8
8
 
9
- `@uncinq/css-components` is the top of the stack: 30 components and 1 utility, written as plain CSS, reading their values from [@uncinq/component-tokens](../../component-tokens/) and adding no values of their own.
9
+ `@uncinq/css-components` is the top of the stack: 31 components and 1 utility, written as plain CSS, reading their values from [@uncinq/component-tokens](../../component-tokens/) and adding no values of their own.
10
10
 
11
11
  ```
12
12
  @uncinq/design-tokens primitive + semantic values
@@ -25,7 +25,7 @@ One page per component, grouped into six sections.
25
25
  | --- | --- |
26
26
  | [Buttons](../buttons/) | [`.btn`](../buttons/btn/), [`.btn-close`](../buttons/btn-close/), [`.btn-menu`](../buttons/btn-menu/), [`.btn-search`](../buttons/btn-search/), [`.btn-filter`](../buttons/btn-filter/), [`.btn-share`](../buttons/btn-share/), [`.btn-toc`](../buttons/btn-toc/), [`.btn-toggle-video`](../buttons/btn-toggle-video/) |
27
27
  | [Overlays](../overlays/) | [panel](../overlays/panel/), [`.modal`](../overlays/modal/), [`.drawer`](../overlays/drawer/), [`.dropdown`](../overlays/dropdown/) |
28
- | [Content](../content/) | [`.alert`](../content/alert/), [`.badge`](../content/badge/), [`.banner`](../content/banner/), [`.card`](../content/card/), [`.items`](../content/items/), [`.list`](../content/list/), [`.media`](../content/media/), [`.push`](../content/push/), [`.surtitle`](../content/surtitle/) |
28
+ | [Content](../content/) | [`.alert`](../content/alert/), [`.badge`](../content/badge/), [`.banner`](../content/banner/), [`.card`](../content/card/), [`.items`](../content/items/), [`.list`](../content/list/), [`.media`](../content/media/), [`.push`](../content/push/), [`.rating`](../content/rating/), [`.surtitle`](../content/surtitle/) |
29
29
  | [Navigation](../navigation/) | [`.nav`](../navigation/nav/), [`.nav-title`](../navigation/nav-title/), [`.nav-accessibility`](../navigation/nav-accessibility/), [`.breadcrumb-wrapper`](../navigation/breadcrumb/), [`.pagination`](../navigation/pagination/) |
30
30
  | [Embeds](../embeds/) | [`.embed`](../embeds/embed/), [`.video`](../embeds/video/), [`.map`](../embeds/map/) |
31
31
  | [Forms](../forms/) | [`.form`](../forms/form/), [`.form-check`](../forms/form-check/) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uncinq/css-components",
3
- "version": "1.27.3",
3
+ "version": "1.28.0",
4
4
  "description": "Framework-agnostic CSS component implementations.",
5
5
  "license": "MIT",
6
6
  "repository": {