@atelic-action/ui 0.8.0 → 0.10.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
@@ -41,7 +41,7 @@ The package sheets sit inside `@layer atelic-ui`, so any rule a site writes outs
41
41
  | Import | Exports |
42
42
  |---|---|
43
43
  | `@atelic-action/ui/chrome` | `SiteHeader`, `SiteMenu`, `Footer`, `CreditBar`, `StickyCTABar`, `BrandLockup`, `SkipLink`, and their prop types |
44
- | `@atelic-action/ui/components` | `NotFound` and the page components (`Checklist`, `DataTable`, `StatRow`, `Callout`, `Collapsible`, `StackedBar`, `ColumnChart`, `ThresholdScale`), with their prop types (see [The Not Found Page](#the-not-found-page) and [Page Components](#page-components)) |
44
+ | `@atelic-action/ui/components` | `NotFound` and the page components (`Checklist`, `DataTable`, `StatRow`, `Callout`, `Collapsible`, `StackedBar`, `ColumnChart`, `ThresholdScale`, `StarRating`), with their prop types (see [The Not Found Page](#the-not-found-page) and [Page Components](#page-components)) |
45
45
  | `@atelic-action/ui/email` | The email components, the theme provider, the plain text helpers, and their prop types (see [Email](#email)) |
46
46
  | `@atelic-action/ui/email/render` | `renderEmail` and `renderFailureEmail`, the only entry that imports `react-dom/server` |
47
47
  | `@atelic-action/ui/tokens` | `Palette`, `atelicPalette`, `Fonts`, `atelicFonts`, `toThemeCSS`, `themeTokenMap` |
@@ -50,6 +50,7 @@ The package sheets sit inside `@layer atelic-ui`, so any rule a site writes outs
50
50
  | `@atelic-action/ui/styles/base.css` | Resets, the `.mkt` canvas, typography, and layout helpers |
51
51
  | `@atelic-action/ui/styles/chrome.css` | Styles for everything under `chrome` |
52
52
  | `@atelic-action/ui/styles/components.css` | Layout defaults for everything under `components` |
53
+ | `@atelic-action/ui/styles/star-rating.css` | `StarRating`'s styles alone, for an app that wears none of the site chrome; `components.css` already imports it |
53
54
 
54
55
  Every component renders from props alone. None reads a config file or a router, so a site maps its own config onto the props in its shell:
55
56
 
@@ -261,9 +262,61 @@ The blocks a page composes, each styled in `styles/components.css` inside the `a
261
262
  | `StackedBar` | One bar split into the parts of a whole, with a key | A part of zero is keyed and not drawn |
262
263
  | `ColumnChart` | A bar a period, the periods in groups that carry a label and a total | Months in quarters is the case it was cut for: a quarter reads against the one before it and the same one a year earlier |
263
264
  | `ThresholdScale` | Zones on a scale and a pin for each reading | For a number judged against lines, like a load time against good and poor |
265
+ | `StarRating` | A row of stars showing a rating in half star steps, and taking one when given `onChange` | Each star is two hit targets; picking the current rating clears it. Editable, it is one slider to a keyboard and a screen reader; read only, a picture named by the rating. `fillColor` and `outlineColor` take any CSS color, `size` and `max` set the star and the scale |
266
+
267
+ **StarRating outside a site.** It is the one component that does not sit under `.mkt` or read a theme token: its styles are in `styles/star-rating.css`, and its two colors are the `--star-fill` and `--star-outline` custom properties, falling back to the text color and a faint tint of it. An app that installs the package for this component alone imports that one sheet, not `components.css`.
268
+
269
+ **A part's own color.** `StackedBar` takes a `color` on a segment, any CSS color, for parts that are not good or bad (the protein, carbs, and fat of a meal), and it wins over `tone`. `showKey={false}` draws the bar alone for a page that keys the parts its own way; the bar keeps its spoken summary.
264
270
 
265
271
  **Tones.** `good`, `warn`, `bad`, `info`, and `muted` are the one palette the package carries, because good, a warning, and broken read green, amber, and red whatever the brand is. A component sets `data-tone`, and `--tone`, `--tone-fill`, and `--tone-bg` follow for whatever sits inside. A site's theme may restate `--tone-good` and the rest on `.mkt`; `info` and `muted` come from the theme's own primary and neutral tokens.
266
272
 
273
+ ## In an App
274
+
275
+ The page components also work in an app that has its own base styles (Tailwind, shadcn) and wears none of the site chrome. Three things make that so (0.10.0):
276
+
277
+ 1. **A second scope.** Every rule in `styles/components.css` answers to `.atelic-ui` as well as `.mkt`. `.atelic-ui` has no resets behind it, so an app puts the class on the region that holds a package component and nothing else in it restyles. Prefer a region to `body`: the components use plain class names (`.stat .num`, `.callout`, `.note`), and an app with markup of its own under those names would have a page wide scope reach it. `body` is right only for an app that has checked it has none, and it is the one way to cover a component rendered in a portal.
278
+ 2. **The app imports one sheet**, `@atelic-action/ui/styles/components.css`, and neither `base.css` nor `chrome.css`. An app's own unlayered CSS always wins over the package. With Tailwind, where the app can declare the layer order before Tailwind loads, `@layer theme, base, atelic-ui, components, utilities;` keeps utilities winning over the package too; where it cannot, the package's layer lands last and wins over utilities set on a package element, which only matters to a caller styling one with a utility class.
279
+ 3. **The app maps its tokens** onto the ones the components read, in its own stylesheet on the same scope:
280
+
281
+ | Token | What it colors |
282
+ |---|---|
283
+ | `--ink`, `--ink-soft` | Text and quieter text |
284
+ | `--neutral-100`, `--neutral-200`, `--neutral-500` | Hairlines, tracks, and captions |
285
+ | `--surface-alt` | The quiet ground behind a muted tone |
286
+ | `--ui-primary` (falls back to `--primary`), `--primary-bright`, `--primary-tint` | The accent, its brighter fill, and its tint |
287
+ | `--ui-card` (falls back to `--card`) | A raised surface |
288
+ | `--font-sans`, `--font-mono`, `--radius-sm` | Type and the small radius |
289
+ | `--tone-good`, `--tone-warn`, `--tone-bad`, each with `-fill` and `-bg` | Optional: the package defines these, and a dark theme restates them |
290
+
291
+ `--ui-primary` and `--ui-card` exist because an app's own `--primary` and `--card` may hold something else: shadcn keeps bare HSL channels there, which are not a color on their own. A site sets neither and nothing changes.
292
+
293
+ ```css
294
+ /* app.css */
295
+ .atelic-ui {
296
+ --ink: hsl(var(--foreground));
297
+ --ink-soft: hsl(var(--muted-foreground));
298
+ --neutral-500: hsl(var(--muted-foreground));
299
+ --neutral-200: hsl(var(--border));
300
+ --neutral-100: hsl(var(--muted));
301
+ --surface-alt: hsl(var(--muted));
302
+ --ui-primary: hsl(var(--brand));
303
+ --ui-card: hsl(var(--card));
304
+ }
305
+ ```
306
+
307
+ `StarRating` needs none of this: its sheet is unscoped and reads no token. The chrome (`SiteHeader`, `Footer`, and the rest) and `NotFound` remain site shaped and are not meant for a signed in app.
308
+
309
+ ## The Gallery
310
+
311
+ `gallery/` is one static page, The Components, that shows every page component on its own with made up data for a fictional bakery, wearing the practice's own palette and type. It lives at [ui.atelic.me](https://ui.atelic.me), noindexed and disallowed in its `robots.txt`, since it is for the people building with the package and never search inventory. It imports the components and stylesheets from `src/` by relative path rather than from npm, so it always shows the code in the repo, and it reads the package name and version from `package.json` at build time. It sits outside `src/`, so it is never published.
312
+
313
+ ```bash
314
+ bun run gallery:dev # a dev server on the repo's own source
315
+ bun run gallery:build # the static page, written to gallery/dist
316
+ ```
317
+
318
+ `vercel.json` builds it the same way and serves `gallery/dist`. Typecheck and lint cover it, and CI builds it, so a component change that breaks the gallery fails CI. **A new page component gets a gallery section in the same change**, with each of its meaningfully different forms shown.
319
+
267
320
  ## Releasing
268
321
 
269
322
  1. Bump `version` in `package.json` and merge it to `main`: patch for a fix, minor for a new component or prop, major for a breaking prop or class rename.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atelic-action/ui",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Shared UI for the Atelic templates: site chrome, base styles, and the component library they install",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -28,13 +28,16 @@
28
28
  "./styles/base.css": "./src/styles/base.css",
29
29
  "./styles/chrome.css": "./src/styles/chrome.css",
30
30
  "./styles/components.css": "./src/styles/components.css",
31
+ "./styles/star-rating.css": "./src/styles/star-rating.css",
31
32
  "./package.json": "./package.json"
32
33
  },
33
34
  "scripts": {
34
35
  "lint": "biome check .",
35
36
  "lint:fix": "biome check . --write",
36
- "typecheck": "tsc --noEmit",
37
- "test:run": "vitest run"
37
+ "typecheck": "tsc --noEmit && tsc --noEmit -p gallery",
38
+ "test:run": "vitest run",
39
+ "gallery:dev": "vite gallery",
40
+ "gallery:build": "vite build gallery"
38
41
  },
39
42
  "peerDependencies": {
40
43
  "lucide-react": "^1.23.0",
@@ -47,11 +50,13 @@
47
50
  "@testing-library/react": "^16.0.1",
48
51
  "@types/react": "^19.0.0",
49
52
  "@types/react-dom": "^19.0.0",
53
+ "@vitejs/plugin-react": "^6.1.1",
50
54
  "jsdom": "^25.0.1",
51
55
  "lucide-react": "^1.23.0",
52
56
  "react": "^19.0.0",
53
57
  "react-dom": "^19.0.0",
54
58
  "typescript": "^5.6.3",
59
+ "vite": "^8.3.2",
55
60
  "vitest": "^4.0.0"
56
61
  },
57
62
  "trustedDependencies": [
@@ -1,3 +1,4 @@
1
+ import type { CSSProperties } from "react";
1
2
  import type { Tone } from "./tone";
2
3
 
3
4
  export interface StackedBarSegment {
@@ -5,16 +6,26 @@ export interface StackedBarSegment {
5
6
  value: number;
6
7
  /** The figure as printed, e.g. "1,158"; defaults to the value with separators. */
7
8
  display?: string;
8
- tone: Tone;
9
+ /** How the part reads. Leave it out when `color` says it instead. */
10
+ tone?: Tone;
11
+ /** A color of the part's own, any CSS color, for parts that are not good or bad. It wins over `tone`. */
12
+ color?: string;
9
13
  }
10
14
 
11
15
  export interface StackedBarProps {
12
16
  segments: StackedBarSegment[];
17
+ /** False draws the bar alone, for a page that keys the parts its own way. The bar keeps its spoken summary. */
18
+ showKey?: boolean;
13
19
  className?: string;
14
20
  }
15
21
 
22
+ /** A part's own color, set as the fill its bar and its swatch both read. */
23
+ function fill(segment: StackedBarSegment): CSSProperties | undefined {
24
+ return segment.color ? ({ "--tone-fill": segment.color } as CSSProperties) : undefined;
25
+ }
26
+
16
27
  /** One bar split into the parts of a whole, with a key naming each part and its figure. */
17
- export function StackedBar({ segments, className }: StackedBarProps) {
28
+ export function StackedBar({ segments, showKey = true, className }: StackedBarProps) {
18
29
  const total = segments.reduce((sum, segment) => sum + segment.value, 0) || 1;
19
30
  const figure = (segment: StackedBarSegment) =>
20
31
  segment.display ?? segment.value.toLocaleString("en-US");
@@ -28,19 +39,21 @@ export function StackedBar({ segments, className }: StackedBarProps) {
28
39
  <span
29
40
  key={segment.label}
30
41
  data-tone={segment.tone}
31
- style={{ flexGrow: segment.value / total }}
42
+ style={{ flexGrow: segment.value / total, ...fill(segment) }}
32
43
  />
33
44
  ))}
34
45
  </div>
35
- <ul className="chart-key">
36
- {segments.map((segment) => (
37
- <li key={segment.label} data-tone={segment.tone}>
38
- <span className="chart-swatch" aria-hidden="true" />
39
- <b>{figure(segment)}</b>
40
- <span>{segment.label}</span>
41
- </li>
42
- ))}
43
- </ul>
46
+ {showKey && (
47
+ <ul className="chart-key">
48
+ {segments.map((segment) => (
49
+ <li key={segment.label} data-tone={segment.tone} style={fill(segment)}>
50
+ <span className="chart-swatch" aria-hidden="true" />
51
+ <b>{figure(segment)}</b>
52
+ <span>{segment.label}</span>
53
+ </li>
54
+ ))}
55
+ </ul>
56
+ )}
44
57
  </div>
45
58
  );
46
59
  }
@@ -0,0 +1,153 @@
1
+ import { Star } from "lucide-react";
2
+ import { type CSSProperties, type KeyboardEvent, useState } from "react";
3
+
4
+ const STEP = 0.5;
5
+
6
+ export interface StarRatingProps {
7
+ /** The rating, in half star steps from 0.5 up to `max`; null is unrated. */
8
+ value: number | null;
9
+ /** Called with the new rating, or null to clear it. Leave it out for a read only row. */
10
+ onChange?: (value: number | null) => void;
11
+ /** How many stars the scale runs to. */
12
+ max?: number;
13
+ /** The side of one star, in pixels. */
14
+ size?: number;
15
+ /** The color of a filled star: any CSS color. Defaults to the text color around it. */
16
+ fillColor?: string;
17
+ /** The color of a star's outline: any CSS color. Defaults to a faint tint of the text color. */
18
+ outlineColor?: string;
19
+ /** What a screen reader calls the row. */
20
+ label?: string;
21
+ className?: string;
22
+ }
23
+
24
+ /** "4.5 stars", "1 star", "Not rated": the rating as a person would say it. */
25
+ export function describeStarRating(value: number | null): string {
26
+ if (value === null) return "Not rated";
27
+ return `${value} ${value === 1 ? "star" : "stars"}`;
28
+ }
29
+
30
+ /**
31
+ * A row of stars that shows a rating in half star steps and, given `onChange`,
32
+ * takes one. Each star is two hit targets, its left half and its right, so a
33
+ * pointer lands on 3.5 or 4 without a second control. Picking the rating
34
+ * already set clears it, which is the only way back to unrated and needs no
35
+ * button of its own.
36
+ *
37
+ * To a keyboard and a screen reader an editable row is one slider: the arrows
38
+ * move half a star, Home clears, End is the top of the scale. The halves stay
39
+ * out of the tab order, so the row is one stop rather than ten. Without
40
+ * `onChange` it is a picture with the rating as its name.
41
+ *
42
+ * The two colors are the `--star-fill` and `--star-outline` custom properties.
43
+ * `fillColor` and `outlineColor` set them on this row; a site may also set
44
+ * them in its own CSS for every row at once.
45
+ */
46
+ export function StarRating({
47
+ value,
48
+ onChange,
49
+ max = 5,
50
+ size = 24,
51
+ fillColor,
52
+ outlineColor,
53
+ label = "Rating",
54
+ className,
55
+ }: StarRatingProps) {
56
+ const [hover, setHover] = useState<number | null>(null);
57
+ const shown = hover ?? value ?? 0;
58
+
59
+ const style = {
60
+ "--star-size": `${size}px`,
61
+ ...(fillColor ? { "--star-fill": fillColor } : {}),
62
+ ...(outlineColor ? { "--star-outline": outlineColor } : {}),
63
+ } as CSSProperties;
64
+ const classes = ["star-rating", className].filter(Boolean).join(" ");
65
+
66
+ const stars = Array.from({ length: max }, (_, i) => i + 1).map((star) => {
67
+ const fill = shown >= star ? "100%" : shown >= star - STEP ? "50%" : "0%";
68
+ return (
69
+ <span key={star} className="star-rating-star">
70
+ <Star aria-hidden="true" size={size} className="star-rating-outline" />
71
+ <span aria-hidden="true" className="star-rating-fill" style={{ width: fill }}>
72
+ <Star size={size} />
73
+ </span>
74
+ {onChange &&
75
+ [star - STEP, star].map((rating, half) => (
76
+ <button
77
+ key={rating}
78
+ type="button"
79
+ tabIndex={-1}
80
+ aria-label={describeStarRating(rating)}
81
+ className={`star-rating-hit ${half === 0 ? "is-left" : "is-right"}`}
82
+ onMouseEnter={() => setHover(rating)}
83
+ onClick={() => {
84
+ // Drop the preview with the pick: the pointer is still on this
85
+ // half, and a preview left up would hide a clear behind the
86
+ // very stars it just emptied.
87
+ setHover(null);
88
+ onChange(rating === value ? null : rating);
89
+ }}
90
+ />
91
+ ))}
92
+ </span>
93
+ );
94
+ });
95
+
96
+ if (!onChange) {
97
+ return (
98
+ <span
99
+ role="img"
100
+ aria-label={`${label}: ${describeStarRating(value)}`}
101
+ className={classes}
102
+ style={style}
103
+ >
104
+ {stars}
105
+ </span>
106
+ );
107
+ }
108
+
109
+ const set = (next: number | null) => {
110
+ if (next !== value) onChange(next);
111
+ };
112
+
113
+ const onKeyDown = (event: KeyboardEvent) => {
114
+ const current = value ?? 0;
115
+ switch (event.key) {
116
+ case "ArrowRight":
117
+ case "ArrowUp":
118
+ set(Math.min(max, current + STEP));
119
+ break;
120
+ case "ArrowLeft":
121
+ case "ArrowDown":
122
+ set(current - STEP > 0 ? current - STEP : null);
123
+ break;
124
+ case "Home":
125
+ set(null);
126
+ break;
127
+ case "End":
128
+ set(max);
129
+ break;
130
+ default:
131
+ return;
132
+ }
133
+ event.preventDefault();
134
+ };
135
+
136
+ return (
137
+ <span
138
+ role="slider"
139
+ tabIndex={0}
140
+ aria-label={label}
141
+ aria-valuemin={0}
142
+ aria-valuemax={max}
143
+ aria-valuenow={value ?? 0}
144
+ aria-valuetext={describeStarRating(value)}
145
+ className={`${classes} is-editable`}
146
+ style={style}
147
+ onKeyDown={onKeyDown}
148
+ onMouseLeave={() => setHover(null)}
149
+ >
150
+ {stars}
151
+ </span>
152
+ );
153
+ }
@@ -18,6 +18,7 @@ export {
18
18
  } from "./DataTable";
19
19
  export { NotFound, type NotFoundProps } from "./NotFound";
20
20
  export { StackedBar, type StackedBarProps, type StackedBarSegment } from "./StackedBar";
21
+ export { describeStarRating, StarRating, type StarRatingProps } from "./StarRating";
21
22
  export {
22
23
  type StatDelta,
23
24
  type StatDirection,
@@ -1,20 +1,34 @@
1
+ @import "./star-rating.css";
2
+
1
3
  /* Layout defaults for the components under src/components. A site's own
2
4
  classes (.page-hero, .eyebrow, .lead, .btn) arrive unlayered and win, so
3
- these only fill in where a site does not style them. */
5
+ these only fill in where a site does not style them.
6
+
7
+ Every rule answers to either of two scopes. `.mkt` is a site's canvas,
8
+ which base.css also resets headings, links, and buttons under.
9
+ `.atelic-ui` is the same scope with no resets behind it, for an app that
10
+ has its own base (Tailwind, shadcn) and wants only the components. The
11
+ two weigh the same, so nothing a site already overrides changes.
12
+
13
+ Two tokens are read through names of the package's own first:
14
+ `--ui-primary` and `--ui-card`, each falling back to `--primary` and
15
+ `--card`. A site sets neither and nothing changes. An app whose own
16
+ `--primary` and `--card` hold something else (shadcn keeps bare HSL
17
+ channels there) sets the `--ui-` names and leaves its own alone. */
4
18
  @layer atelic-ui {
5
- .mkt .not-found-hero {
19
+ :is(.mkt, .atelic-ui) .not-found-hero {
6
20
  padding-top: calc(var(--nav-height, 72px) + clamp(40px, 7vw, 86px));
7
21
  padding-bottom: clamp(28px, 5vw, 60px);
8
22
  }
9
- .mkt .not-found-hero h1 {
23
+ :is(.mkt, .atelic-ui) .not-found-hero h1 {
10
24
  margin: 16px 0 0;
11
25
  }
12
- .mkt .not-found-hero .lead {
26
+ :is(.mkt, .atelic-ui) .not-found-hero .lead {
13
27
  margin-top: 22px;
14
28
  max-width: 56ch;
15
29
  }
16
- .mkt .not-found-links .cta-row,
17
- .mkt .not-found-closing .cta-row {
30
+ :is(.mkt, .atelic-ui) .not-found-links .cta-row,
31
+ :is(.mkt, .atelic-ui) .not-found-closing .cta-row {
18
32
  margin-top: 22px;
19
33
  }
20
34
 
@@ -22,7 +36,7 @@
22
36
  warning, and broken read green, amber, and red whatever the brand is.
23
37
  A site's theme may restate any of these on .mkt. A component sets
24
38
  data-tone, and --tone, --tone-fill, and --tone-bg follow. ---- */
25
- .mkt {
39
+ :is(.mkt, .atelic-ui) {
26
40
  --tone-good: #1b7f3b;
27
41
  --tone-good-fill: #37a35c;
28
42
  --tone-good-bg: #eaf6ee;
@@ -33,27 +47,27 @@
33
47
  --tone-bad-fill: #d9483f;
34
48
  --tone-bad-bg: #fdecea;
35
49
  }
36
- .mkt [data-tone="good"] {
50
+ :is(.mkt, .atelic-ui) [data-tone="good"] {
37
51
  --tone: var(--tone-good);
38
52
  --tone-fill: var(--tone-good-fill);
39
53
  --tone-bg: var(--tone-good-bg);
40
54
  }
41
- .mkt [data-tone="warn"] {
55
+ :is(.mkt, .atelic-ui) [data-tone="warn"] {
42
56
  --tone: var(--tone-warn);
43
57
  --tone-fill: var(--tone-warn-fill);
44
58
  --tone-bg: var(--tone-warn-bg);
45
59
  }
46
- .mkt [data-tone="bad"] {
60
+ :is(.mkt, .atelic-ui) [data-tone="bad"] {
47
61
  --tone: var(--tone-bad);
48
62
  --tone-fill: var(--tone-bad-fill);
49
63
  --tone-bg: var(--tone-bad-bg);
50
64
  }
51
- .mkt [data-tone="info"] {
52
- --tone: var(--primary);
65
+ :is(.mkt, .atelic-ui) [data-tone="info"] {
66
+ --tone: var(--ui-primary, var(--primary));
53
67
  --tone-fill: var(--primary-bright);
54
68
  --tone-bg: var(--primary-tint);
55
69
  }
56
- .mkt [data-tone="muted"] {
70
+ :is(.mkt, .atelic-ui) [data-tone="muted"] {
57
71
  --tone: var(--neutral-500);
58
72
  --tone-fill: var(--neutral-200);
59
73
  --tone-bg: var(--surface-alt);
@@ -61,50 +75,50 @@
61
75
 
62
76
  /* ---- StatRow: large figures, each over what it counts, its period,
63
77
  and its comparisons ---- */
64
- .mkt .stat-row {
78
+ :is(.mkt, .atelic-ui) .stat-row {
65
79
  display: grid;
66
80
  gap: 24px;
67
81
  grid-template-columns: repeat(2, minmax(0, 1fr));
68
82
  }
69
83
  @media (min-width: 760px) {
70
- .mkt .stat-row {
84
+ :is(.mkt, .atelic-ui) .stat-row {
71
85
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
72
86
  }
73
87
  }
74
- .mkt .stat-row.is-center .stat {
88
+ :is(.mkt, .atelic-ui) .stat-row.is-center .stat {
75
89
  text-align: center;
76
90
  }
77
- .mkt .stat .num {
91
+ :is(.mkt, .atelic-ui) .stat .num {
78
92
  display: flex;
79
93
  align-items: center;
80
94
  gap: 0.12em;
81
95
  font-size: clamp(2.2rem, 5vw, 3rem);
82
96
  font-weight: 800;
83
97
  letter-spacing: -0.03em;
84
- color: var(--tone, var(--primary));
98
+ color: var(--tone, var(--ui-primary, var(--primary)));
85
99
  line-height: 1;
86
100
  font-variant-numeric: tabular-nums;
87
101
  }
88
- .mkt .stat-row.is-center .stat .num {
102
+ :is(.mkt, .atelic-ui) .stat-row.is-center .stat .num {
89
103
  justify-content: center;
90
104
  }
91
- .mkt .stat-trend {
105
+ :is(.mkt, .atelic-ui) .stat-trend {
92
106
  width: 0.8em;
93
107
  height: 0.8em;
94
108
  flex: none;
95
109
  }
96
- .mkt .stat .lbl {
110
+ :is(.mkt, .atelic-ui) .stat .lbl {
97
111
  font-size: 0.95rem;
98
112
  font-weight: 650;
99
113
  color: var(--ink);
100
114
  margin-top: 8px;
101
115
  }
102
- .mkt .stat-period {
116
+ :is(.mkt, .atelic-ui) .stat-period {
103
117
  margin-left: 0.5em;
104
118
  font-weight: 400;
105
119
  color: var(--neutral-500);
106
120
  }
107
- .mkt .stat-delta {
121
+ :is(.mkt, .atelic-ui) .stat-delta {
108
122
  display: flex;
109
123
  flex-wrap: wrap;
110
124
  align-items: baseline;
@@ -115,27 +129,27 @@
115
129
  color: var(--neutral-500);
116
130
  font-variant-numeric: tabular-nums;
117
131
  }
118
- .mkt .stat-delta-change {
132
+ :is(.mkt, .atelic-ui) .stat-delta-change {
119
133
  display: inline-flex;
120
134
  align-items: center;
121
135
  gap: 2px;
122
136
  font-weight: 700;
123
137
  color: var(--tone);
124
138
  }
125
- .mkt .stat-delta-change svg {
139
+ :is(.mkt, .atelic-ui) .stat-delta-change svg {
126
140
  align-self: center;
127
141
  }
128
- .mkt .stat-delta-basis {
142
+ :is(.mkt, .atelic-ui) .stat-delta-basis {
129
143
  text-decoration: none;
130
144
  }
131
- .mkt .stat .note {
145
+ :is(.mkt, .atelic-ui) .stat .note {
132
146
  font-size: 0.8rem;
133
147
  color: var(--neutral-500);
134
148
  margin-top: 4px;
135
149
  }
136
150
 
137
151
  /* ---- Collapsible: a labeled line that opens to the detail behind it ---- */
138
- .mkt .collapsible > summary {
152
+ :is(.mkt, .atelic-ui) .collapsible > summary {
139
153
  display: flex;
140
154
  align-items: center;
141
155
  justify-content: space-between;
@@ -143,77 +157,77 @@
143
157
  cursor: pointer;
144
158
  list-style: none;
145
159
  }
146
- .mkt .collapsible > summary::-webkit-details-marker {
160
+ :is(.mkt, .atelic-ui) .collapsible > summary::-webkit-details-marker {
147
161
  display: none;
148
162
  }
149
- .mkt .collapsible-text {
163
+ :is(.mkt, .atelic-ui) .collapsible-text {
150
164
  display: flex;
151
165
  flex-direction: column;
152
166
  gap: 2px;
153
167
  min-width: 0;
154
168
  }
155
- .mkt .collapsible-label {
169
+ :is(.mkt, .atelic-ui) .collapsible-label {
156
170
  font-weight: 650;
157
171
  color: var(--ink);
158
172
  }
159
- .mkt .collapsible-summary {
173
+ :is(.mkt, .atelic-ui) .collapsible-summary {
160
174
  color: var(--ink-soft);
161
175
  text-wrap: pretty;
162
176
  }
163
- .mkt .collapsible > summary:hover .collapsible-label {
164
- color: var(--primary);
177
+ :is(.mkt, .atelic-ui) .collapsible > summary:hover .collapsible-label {
178
+ color: var(--ui-primary, var(--primary));
165
179
  }
166
- .mkt .collapsible-chevron {
180
+ :is(.mkt, .atelic-ui) .collapsible-chevron {
167
181
  flex: none;
168
182
  color: var(--neutral-500);
169
183
  transition: transform 0.15s;
170
184
  }
171
- .mkt .collapsible[open] > summary .collapsible-chevron {
185
+ :is(.mkt, .atelic-ui) .collapsible[open] > summary .collapsible-chevron {
172
186
  transform: rotate(180deg);
173
187
  }
174
- .mkt .collapsible-body {
188
+ :is(.mkt, .atelic-ui) .collapsible-body {
175
189
  margin-top: 12px;
176
190
  }
177
191
  @media (prefers-reduced-motion: reduce) {
178
- .mkt .collapsible-chevron {
192
+ :is(.mkt, .atelic-ui) .collapsible-chevron {
179
193
  transition: none;
180
194
  }
181
195
  }
182
196
  /* Paper cannot be clicked, so a printed collapsible is open. */
183
197
  @media print {
184
- .mkt .collapsible::details-content {
198
+ :is(.mkt, .atelic-ui) .collapsible::details-content {
185
199
  content-visibility: visible;
186
200
  display: block;
187
201
  }
188
- .mkt .collapsible-chevron {
202
+ :is(.mkt, .atelic-ui) .collapsible-chevron {
189
203
  display: none;
190
204
  }
191
205
  }
192
206
 
193
207
  /* ---- Callout: an icon in its tone beside the title, the explainer
194
208
  under it; boxed, it sits apart on a tinted ground ---- */
195
- .mkt .callout {
209
+ :is(.mkt, .atelic-ui) .callout {
196
210
  display: grid;
197
211
  grid-template-columns: auto minmax(0, 1fr);
198
212
  gap: 10px;
199
213
  align-items: start;
200
214
  }
201
- .mkt .callout-icon {
215
+ :is(.mkt, .atelic-ui) .callout-icon {
202
216
  margin-top: 0.15em;
203
217
  color: var(--tone);
204
218
  flex: none;
205
219
  }
206
- .mkt .callout-title {
220
+ :is(.mkt, .atelic-ui) .callout-title {
207
221
  font-weight: 650;
208
222
  color: var(--ink);
209
223
  text-wrap: pretty;
210
224
  }
211
- .mkt .callout-body {
225
+ :is(.mkt, .atelic-ui) .callout-body {
212
226
  margin-top: 2px;
213
227
  color: var(--ink-soft);
214
228
  text-wrap: pretty;
215
229
  }
216
- .mkt .callout.is-box {
230
+ :is(.mkt, .atelic-ui) .callout.is-box {
217
231
  padding: 14px 16px;
218
232
  border-radius: var(--radius-sm);
219
233
  background: var(--tone-bg);
@@ -221,7 +235,7 @@
221
235
  }
222
236
 
223
237
  /* ---- Checklist: checks passed or failed, two columns when there is room ---- */
224
- .mkt .checklist {
238
+ :is(.mkt, .atelic-ui) .checklist {
225
239
  list-style: none;
226
240
  margin: 0;
227
241
  padding: 0;
@@ -230,36 +244,36 @@
230
244
  gap: 12px 28px;
231
245
  }
232
246
  @media print, (min-width: 721px) {
233
- .mkt .checklist {
247
+ :is(.mkt, .atelic-ui) .checklist {
234
248
  grid-template-columns: repeat(2, minmax(0, 1fr));
235
249
  }
236
250
  }
237
- .mkt .checklist li {
251
+ :is(.mkt, .atelic-ui) .checklist li {
238
252
  display: grid;
239
253
  grid-template-columns: auto minmax(0, 1fr);
240
254
  gap: 10px;
241
255
  align-items: start;
242
256
  line-height: 1.4;
243
257
  }
244
- .mkt .checklist li > svg {
258
+ :is(.mkt, .atelic-ui) .checklist li > svg {
245
259
  margin-top: 0.1em;
246
260
  color: var(--tone);
247
261
  }
248
- .mkt .checklist-label {
262
+ :is(.mkt, .atelic-ui) .checklist-label {
249
263
  display: block;
250
264
  font-weight: 600;
251
265
  color: var(--ink);
252
266
  }
253
- .mkt .checklist li[data-tone="bad"] .checklist-label {
267
+ :is(.mkt, .atelic-ui) .checklist li[data-tone="bad"] .checklist-label {
254
268
  color: var(--tone);
255
269
  }
256
- .mkt .checklist-detail {
270
+ :is(.mkt, .atelic-ui) .checklist-detail {
257
271
  display: block;
258
272
  color: var(--ink-soft);
259
273
  }
260
274
 
261
275
  /* ---- The charts' shared key: a swatch, a figure, a label ---- */
262
- .mkt .chart-key {
276
+ :is(.mkt, .atelic-ui) .chart-key {
263
277
  list-style: none;
264
278
  margin: 10px 0 0;
265
279
  padding: 0;
@@ -269,17 +283,17 @@
269
283
  font-size: 0.82rem;
270
284
  color: var(--ink-soft);
271
285
  }
272
- .mkt .chart-key li {
286
+ :is(.mkt, .atelic-ui) .chart-key li {
273
287
  display: inline-flex;
274
288
  align-items: baseline;
275
289
  gap: 7px;
276
290
  }
277
- .mkt .chart-key b {
291
+ :is(.mkt, .atelic-ui) .chart-key b {
278
292
  font-weight: 650;
279
293
  color: var(--ink);
280
294
  font-variant-numeric: tabular-nums;
281
295
  }
282
- .mkt .chart-swatch {
296
+ :is(.mkt, .atelic-ui) .chart-swatch {
283
297
  width: 10px;
284
298
  height: 10px;
285
299
  border-radius: 3px;
@@ -289,35 +303,35 @@
289
303
  }
290
304
 
291
305
  /* ---- StackedBar: one bar, the parts of a whole ---- */
292
- .mkt .stacked-bar-track {
306
+ :is(.mkt, .atelic-ui) .stacked-bar-track {
293
307
  display: flex;
294
308
  gap: 2px;
295
309
  height: 16px;
296
310
  border-radius: 999px;
297
311
  overflow: hidden;
298
312
  }
299
- .mkt .stacked-bar-track span {
313
+ :is(.mkt, .atelic-ui) .stacked-bar-track span {
300
314
  background: var(--tone-fill);
301
315
  min-width: 4px;
302
316
  }
303
317
 
304
318
  /* ---- ColumnChart: a bar a period, the periods in labeled groups ---- */
305
- .mkt .column-chart-plot {
319
+ :is(.mkt, .atelic-ui) .column-chart-plot {
306
320
  display: flex;
307
321
  align-items: flex-end;
308
322
  gap: 12px;
309
323
  min-width: 0;
310
324
  }
311
- .mkt .column-chart-group {
325
+ :is(.mkt, .atelic-ui) .column-chart-group {
312
326
  flex: 1 1 0;
313
327
  min-width: 0;
314
328
  }
315
- .mkt .column-chart-bars {
329
+ :is(.mkt, .atelic-ui) .column-chart-bars {
316
330
  display: flex;
317
331
  align-items: flex-end;
318
332
  gap: 3px;
319
333
  }
320
- .mkt .column-chart-col {
334
+ :is(.mkt, .atelic-ui) .column-chart-col {
321
335
  flex: 1 1 0;
322
336
  min-width: 0;
323
337
  display: flex;
@@ -325,24 +339,24 @@
325
339
  align-items: center;
326
340
  gap: 3px;
327
341
  }
328
- .mkt .column-chart-value {
342
+ :is(.mkt, .atelic-ui) .column-chart-value {
329
343
  font-size: 0.64rem;
330
344
  font-variant-numeric: tabular-nums;
331
345
  color: var(--ink-soft);
332
346
  white-space: nowrap;
333
347
  }
334
- .mkt .column-chart-bar {
348
+ :is(.mkt, .atelic-ui) .column-chart-bar {
335
349
  width: 100%;
336
350
  max-width: 48px;
337
351
  border-radius: 4px 4px 0 0;
338
352
  background: var(--tone-fill);
339
353
  }
340
- .mkt .column-chart-label {
354
+ :is(.mkt, .atelic-ui) .column-chart-label {
341
355
  font-size: 0.66rem;
342
356
  color: var(--ink-soft);
343
357
  white-space: nowrap;
344
358
  }
345
- .mkt .column-chart-group-label {
359
+ :is(.mkt, .atelic-ui) .column-chart-group-label {
346
360
  display: flex;
347
361
  flex-wrap: wrap;
348
362
  justify-content: center;
@@ -356,7 +370,7 @@
356
370
  color: var(--neutral-500);
357
371
  white-space: nowrap;
358
372
  }
359
- .mkt .column-chart-group-label b {
373
+ :is(.mkt, .atelic-ui) .column-chart-group-label b {
360
374
  font-family: var(--font-sans);
361
375
  font-size: 0.8rem;
362
376
  letter-spacing: 0;
@@ -366,37 +380,37 @@
366
380
  }
367
381
 
368
382
  /* ---- ThresholdScale: zones, and a pin a reading ---- */
369
- .mkt .threshold-scale {
383
+ :is(.mkt, .atelic-ui) .threshold-scale {
370
384
  display: flex;
371
385
  flex-direction: column;
372
386
  gap: 10px;
373
387
  }
374
- .mkt .threshold-scale-row {
388
+ :is(.mkt, .atelic-ui) .threshold-scale-row {
375
389
  display: grid;
376
390
  grid-template-columns: 4.4em minmax(0, 1fr) 3.4em;
377
391
  gap: 10px;
378
392
  align-items: center;
379
393
  }
380
- .mkt .threshold-scale-label {
394
+ :is(.mkt, .atelic-ui) .threshold-scale-label {
381
395
  color: var(--ink-soft);
382
396
  }
383
- .mkt .threshold-scale-track {
397
+ :is(.mkt, .atelic-ui) .threshold-scale-track {
384
398
  position: relative;
385
399
  display: flex;
386
400
  gap: 2px;
387
401
  height: 12px;
388
402
  }
389
- .mkt .threshold-scale-zone {
403
+ :is(.mkt, .atelic-ui) .threshold-scale-zone {
390
404
  background: var(--tone-fill);
391
405
  opacity: 0.32;
392
406
  }
393
- .mkt .threshold-scale-zone:first-child {
407
+ :is(.mkt, .atelic-ui) .threshold-scale-zone:first-child {
394
408
  border-radius: 999px 0 0 999px;
395
409
  }
396
- .mkt .threshold-scale-zone:nth-last-child(2) {
410
+ :is(.mkt, .atelic-ui) .threshold-scale-zone:nth-last-child(2) {
397
411
  border-radius: 0 999px 999px 0;
398
412
  }
399
- .mkt .threshold-scale-pin {
413
+ :is(.mkt, .atelic-ui) .threshold-scale-pin {
400
414
  position: absolute;
401
415
  top: 50%;
402
416
  width: 18px;
@@ -404,30 +418,30 @@
404
418
  margin: -9px 0 0 -9px;
405
419
  border-radius: 50%;
406
420
  background: var(--tone-fill);
407
- border: 3px solid var(--card);
421
+ border: 3px solid var(--ui-card, var(--card));
408
422
  box-shadow: 0 0 0 1.5px var(--tone-fill);
409
423
  }
410
- .mkt .threshold-scale-value {
424
+ :is(.mkt, .atelic-ui) .threshold-scale-value {
411
425
  font-weight: 700;
412
426
  font-variant-numeric: tabular-nums;
413
427
  color: var(--tone);
414
428
  text-align: right;
415
429
  }
416
- .mkt .threshold-scale-axis {
430
+ :is(.mkt, .atelic-ui) .threshold-scale-axis {
417
431
  position: relative;
418
432
  display: flex;
419
433
  gap: 2px;
420
434
  height: 2.3em;
421
435
  font-size: 0.8rem;
422
436
  }
423
- .mkt .threshold-scale-axis > span {
437
+ :is(.mkt, .atelic-ui) .threshold-scale-axis > span {
424
438
  color: var(--tone);
425
439
  font-weight: 600;
426
440
  white-space: nowrap;
427
441
  overflow: hidden;
428
442
  text-overflow: ellipsis;
429
443
  }
430
- .mkt .threshold-scale-axis > i {
444
+ :is(.mkt, .atelic-ui) .threshold-scale-axis > i {
431
445
  position: absolute;
432
446
  top: 1.25em;
433
447
  transform: translateX(-50%);
@@ -441,12 +455,12 @@
441
455
  /* ---- DataTable: on a phone each row stacks, its facts wearing their
442
456
  column's label, and a dropdown sorts; wide, it is a table whose
443
457
  headings sort ---- */
444
- .mkt .data-table table,
445
- .mkt .data-table tbody {
458
+ :is(.mkt, .atelic-ui) .data-table table,
459
+ :is(.mkt, .atelic-ui) .data-table tbody {
446
460
  display: block;
447
461
  width: 100%;
448
462
  }
449
- .mkt .data-table thead {
463
+ :is(.mkt, .atelic-ui) .data-table thead {
450
464
  position: absolute;
451
465
  width: 1px;
452
466
  height: 1px;
@@ -454,7 +468,7 @@
454
468
  clip: rect(0, 0, 0, 0);
455
469
  white-space: nowrap;
456
470
  }
457
- .mkt .data-table-sort {
471
+ :is(.mkt, .atelic-ui) .data-table-sort {
458
472
  display: flex;
459
473
  align-items: center;
460
474
  gap: 8px;
@@ -462,16 +476,16 @@
462
476
  font-size: 0.85rem;
463
477
  color: var(--ink-soft);
464
478
  }
465
- .mkt .data-table-sort select {
479
+ :is(.mkt, .atelic-ui) .data-table-sort select {
466
480
  font: inherit;
467
481
  color: var(--ink);
468
- background: var(--card);
482
+ background: var(--ui-card, var(--card));
469
483
  border: 1px solid var(--neutral-200);
470
484
  border-radius: var(--radius-sm);
471
485
  padding: 5px 8px;
472
486
  max-width: 100%;
473
487
  }
474
- .mkt .data-table tbody tr {
488
+ :is(.mkt, .atelic-ui) .data-table tbody tr {
475
489
  display: flex;
476
490
  flex-wrap: wrap;
477
491
  align-items: center;
@@ -480,25 +494,25 @@
480
494
  border-bottom: 1px solid var(--neutral-100);
481
495
  line-height: 1.35;
482
496
  }
483
- .mkt .data-table tbody tr:first-child {
497
+ :is(.mkt, .atelic-ui) .data-table tbody tr:first-child {
484
498
  border-top: 1.5px solid var(--ink);
485
499
  }
486
- .mkt .data-table tbody tr:last-child {
500
+ :is(.mkt, .atelic-ui) .data-table tbody tr:last-child {
487
501
  border-bottom: 0;
488
502
  padding-bottom: 0;
489
503
  }
490
- .mkt .data-table tbody th {
504
+ :is(.mkt, .atelic-ui) .data-table tbody th {
491
505
  width: 100%;
492
506
  text-align: left;
493
507
  font-weight: 400;
494
508
  }
495
- .mkt .data-table td {
509
+ :is(.mkt, .atelic-ui) .data-table td {
496
510
  display: inline-flex;
497
511
  align-items: center;
498
512
  gap: 6px;
499
513
  font-variant-numeric: tabular-nums;
500
514
  }
501
- .mkt .data-table td::before {
515
+ :is(.mkt, .atelic-ui) .data-table td::before {
502
516
  content: attr(data-label);
503
517
  font-family: var(--font-mono);
504
518
  font-size: 0.7rem;
@@ -507,33 +521,33 @@
507
521
  color: var(--neutral-500);
508
522
  }
509
523
  @media print, (min-width: 721px) {
510
- .mkt .data-table-sort {
524
+ :is(.mkt, .atelic-ui) .data-table-sort {
511
525
  display: none;
512
526
  }
513
- .mkt .data-table table {
527
+ :is(.mkt, .atelic-ui) .data-table table {
514
528
  display: table;
515
529
  border-collapse: collapse;
516
530
  }
517
- .mkt .data-table tbody {
531
+ :is(.mkt, .atelic-ui) .data-table tbody {
518
532
  display: table-row-group;
519
533
  }
520
- .mkt .data-table thead {
534
+ :is(.mkt, .atelic-ui) .data-table thead {
521
535
  position: static;
522
536
  width: auto;
523
537
  height: auto;
524
538
  overflow: visible;
525
539
  clip: auto;
526
540
  }
527
- .mkt .data-table tbody tr {
541
+ :is(.mkt, .atelic-ui) .data-table tbody tr {
528
542
  display: table-row;
529
543
  padding: 0;
530
544
  border: 0;
531
545
  }
532
- .mkt .data-table tbody tr:first-child {
546
+ :is(.mkt, .atelic-ui) .data-table tbody tr:first-child {
533
547
  border-top: 0;
534
548
  }
535
- .mkt .data-table th,
536
- .mkt .data-table td {
549
+ :is(.mkt, .atelic-ui) .data-table th,
550
+ :is(.mkt, .atelic-ui) .data-table td {
537
551
  display: table-cell;
538
552
  padding: 7px 14px 7px 0;
539
553
  border-bottom: 1px solid var(--neutral-100);
@@ -541,23 +555,23 @@
541
555
  vertical-align: middle;
542
556
  font-weight: 400;
543
557
  }
544
- .mkt .data-table tbody th {
558
+ :is(.mkt, .atelic-ui) .data-table tbody th {
545
559
  width: auto;
546
560
  }
547
- .mkt .data-table td::before {
561
+ :is(.mkt, .atelic-ui) .data-table td::before {
548
562
  content: none;
549
563
  }
550
- .mkt .data-table th:last-child,
551
- .mkt .data-table td:last-child {
564
+ :is(.mkt, .atelic-ui) .data-table th:last-child,
565
+ :is(.mkt, .atelic-ui) .data-table td:last-child {
552
566
  padding-right: 0;
553
567
  }
554
- .mkt .data-table [data-align="center"] {
568
+ :is(.mkt, .atelic-ui) .data-table [data-align="center"] {
555
569
  text-align: center;
556
570
  }
557
- .mkt .data-table [data-align="right"] {
571
+ :is(.mkt, .atelic-ui) .data-table [data-align="right"] {
558
572
  text-align: right;
559
573
  }
560
- .mkt .data-table thead th {
574
+ :is(.mkt, .atelic-ui) .data-table thead th {
561
575
  padding-top: 0;
562
576
  padding-bottom: 8px;
563
577
  border-bottom: 1.5px solid var(--ink);
@@ -569,7 +583,7 @@
569
583
  white-space: nowrap;
570
584
  vertical-align: bottom;
571
585
  }
572
- .mkt .data-table thead th button {
586
+ :is(.mkt, .atelic-ui) .data-table thead th button {
573
587
  display: inline-flex;
574
588
  align-items: center;
575
589
  gap: 4px;
@@ -582,23 +596,23 @@
582
596
  color: inherit;
583
597
  cursor: pointer;
584
598
  }
585
- .mkt .data-table thead th button:hover,
586
- .mkt .data-table thead th button[data-active] {
587
- color: var(--primary);
599
+ :is(.mkt, .atelic-ui) .data-table thead th button:hover,
600
+ :is(.mkt, .atelic-ui) .data-table thead th button[data-active] {
601
+ color: var(--ui-primary, var(--primary));
588
602
  }
589
- .mkt .data-table thead th button svg {
603
+ :is(.mkt, .atelic-ui) .data-table thead th button svg {
590
604
  opacity: 0.55;
591
605
  }
592
- .mkt .data-table thead th button[data-active] svg {
606
+ :is(.mkt, .atelic-ui) .data-table thead th button[data-active] svg {
593
607
  opacity: 1;
594
608
  }
595
- .mkt .data-table tbody tr:last-child th,
596
- .mkt .data-table tbody tr:last-child td {
609
+ :is(.mkt, .atelic-ui) .data-table tbody tr:last-child th,
610
+ :is(.mkt, .atelic-ui) .data-table tbody tr:last-child td {
597
611
  border-bottom: 0;
598
612
  }
599
613
  }
600
614
  @media print {
601
- .mkt .data-table thead th button svg {
615
+ :is(.mkt, .atelic-ui) .data-table thead th button svg {
602
616
  display: none;
603
617
  }
604
618
  }
@@ -0,0 +1,67 @@
1
+ /* StarRating: a row of stars in half star steps. On its own sheet, and not
2
+ under .mkt, so an app that wears none of the site chrome can import this
3
+ file alone; components.css imports it for every site.
4
+
5
+ The two colors are custom properties with fallbacks off the text color, so
6
+ the row needs no theme token and takes a color from a prop, from a site's
7
+ own rule, or from whatever it sits in. */
8
+ @layer atelic-ui {
9
+ .star-rating {
10
+ display: inline-flex;
11
+ align-items: center;
12
+ vertical-align: middle;
13
+ line-height: 0;
14
+ border-radius: 4px;
15
+ }
16
+ .star-rating.is-editable:focus-visible {
17
+ outline: 2px solid var(--star-focus, currentColor);
18
+ outline-offset: 2px;
19
+ }
20
+ .star-rating-star {
21
+ position: relative;
22
+ display: inline-block;
23
+ width: var(--star-size, 24px);
24
+ height: var(--star-size, 24px);
25
+ }
26
+ /* Pinned to the star's size: a base reset that caps an svg at its
27
+ container's width would shrink the filled star inside its clip to a
28
+ small whole star, where it has to stay full size and be cut in half. */
29
+ .star-rating .star-rating-star svg {
30
+ display: block;
31
+ width: var(--star-size, 24px);
32
+ height: var(--star-size, 24px);
33
+ max-width: none;
34
+ }
35
+ .star-rating-outline {
36
+ color: var(--star-outline, color-mix(in srgb, currentColor 40%, transparent));
37
+ }
38
+ /* The filled star sits over the outline and is clipped to none, half, or
39
+ all of its width. */
40
+ .star-rating-fill {
41
+ position: absolute;
42
+ inset-block: 0;
43
+ left: 0;
44
+ overflow: hidden;
45
+ pointer-events: none;
46
+ color: var(--star-fill, currentColor);
47
+ }
48
+ .star-rating-fill svg {
49
+ fill: currentColor;
50
+ }
51
+ .star-rating-hit {
52
+ position: absolute;
53
+ inset-block: 0;
54
+ width: 50%;
55
+ margin: 0;
56
+ padding: 0;
57
+ border: 0;
58
+ background: none;
59
+ cursor: pointer;
60
+ }
61
+ .star-rating-hit.is-left {
62
+ left: 0;
63
+ }
64
+ .star-rating-hit.is-right {
65
+ right: 0;
66
+ }
67
+ }