material-inspired-component-library 8.0.0 → 8.0.1
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/.claude/settings.local.json +14 -0
- package/CLAUDE.md +13 -2
- package/components/alert/index.scss +5 -0
- package/components/appbar/index.scss +12 -0
- package/components/badge/index.scss +2 -0
- package/components/bottomsheet/index.scss +9 -0
- package/components/button/index.scss +23 -0
- package/components/card/index.scss +23 -0
- package/components/checkbox/index.scss +17 -0
- package/components/datepicker/index.scss +13 -0
- package/components/dialog/index.scss +16 -0
- package/components/iconbutton/index.scss +18 -0
- package/components/list/index.scss +25 -0
- package/components/menu/index.scss +18 -0
- package/components/navigationrail/index.scss +17 -0
- package/components/progressindicator/README.md +88 -0
- package/components/progressindicator/index.scss +225 -0
- package/components/progressindicator/index.ts +77 -0
- package/components/radio/index.scss +13 -0
- package/components/select/index.scss +6 -0
- package/components/shape/README.md +103 -0
- package/components/shape/_paths.generated.scss +64 -0
- package/components/shape/index.scss +66 -0
- package/components/shape/master.scss +28 -0
- package/components/sidesheet/index.scss +11 -0
- package/components/slider/index.scss +13 -0
- package/components/snackbar/index.scss +12 -0
- package/components/stepper/index.scss +2 -0
- package/components/switch/index.scss +9 -0
- package/components/textfield/index.scss +9 -0
- package/components/timepicker/index.scss +16 -0
- package/dist/alert.css +1 -1
- package/dist/appbar.css +1 -1
- package/dist/badge.css +1 -1
- package/dist/bottomsheet.css +1 -1
- package/dist/button.css +1 -1
- package/dist/card.css +1 -1
- package/dist/checkbox.css +1 -1
- package/dist/components/progressindicator/index.d.ts +6 -0
- package/dist/datepicker.css +1 -1
- package/dist/dialog.css +1 -1
- package/dist/foundations/form/index.js +1 -0
- package/dist/iconbutton.css +1 -1
- package/dist/layout.css +1 -1
- package/dist/list.css +1 -1
- package/dist/menu.css +1 -1
- package/dist/micl.css +1 -1
- package/dist/micl.js +1 -1
- package/dist/navigationrail.css +1 -1
- package/dist/progressindicator.css +1 -0
- package/dist/progressindicator.js +1 -0
- package/dist/radio.css +1 -1
- package/dist/select.css +1 -1
- package/dist/shape.css +1 -0
- package/dist/shape.js +1 -0
- package/dist/sidesheet.css +1 -1
- package/dist/slider.css +1 -1
- package/dist/snackbar.css +1 -1
- package/dist/stepper.css +1 -1
- package/dist/switch.css +1 -1
- package/dist/textfield.css +1 -1
- package/dist/timepicker.css +1 -1
- package/docs/datepicker.html +21 -21
- package/docs/index.html +1 -0
- package/docs/micl.css +1 -1
- package/docs/micl.js +1 -1
- package/docs/progressindicator.html +288 -0
- package/docs/shape.css +1 -0
- package/docs/shape.js +1 -0
- package/docs/shapes.html +86 -21
- package/foundations/layout/README.md +1 -1
- package/foundations/layout/index.scss +3 -0
- package/micl.ts +2 -0
- package/package.json +4 -2
- package/styles/README.md +86 -8
- package/styles/elevation.scss +46 -13
- package/styles/motion.scss +51 -47
- package/styles/shapes.scss +38 -104
- package/styles/statelayer.scss +88 -41
- package/styles/typography.scss +120 -322
- package/styles.scss +10 -6
- package/tools/shapes/check.mjs +42 -0
- package/tools/shapes/generate.mjs +834 -0
- package/webpack.config.js +16 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(node *)",
|
|
5
|
+
"WebSearch",
|
|
6
|
+
"Bash(npm run *)",
|
|
7
|
+
"WebFetch(domain:raw.githubusercontent.com)",
|
|
8
|
+
"WebFetch(domain:android.googlesource.com)",
|
|
9
|
+
"WebFetch(domain:m3.material.io)",
|
|
10
|
+
"WebFetch(domain:github.com)",
|
|
11
|
+
"Bash(xargs grep -l \"body\\\\|html\")"
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
}
|
package/CLAUDE.md
CHANGED
|
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
4
4
|
|
|
5
5
|
## Project Overview
|
|
6
6
|
|
|
7
|
-
MICL is a Material Design 3 UI component library written in
|
|
7
|
+
MICL is a Material Design 3 (M3 Expressive) UI component library written in Sass and TypeScript.
|
|
8
8
|
|
|
9
9
|
## Commands
|
|
10
10
|
|
|
@@ -32,11 +32,22 @@ Each component lives in `components/<name>/` and exports:
|
|
|
32
32
|
### Styling System
|
|
33
33
|
|
|
34
34
|
- Sass uses `@use` (not `@import`); partials are namespaced.
|
|
35
|
-
- Theming is done entirely via CSS custom properties with `--md-
|
|
35
|
+
- Theming is done entirely via CSS custom properties with `--md-comp-*` prefixes.
|
|
36
36
|
- `themes/` contains 11 colour themes, each with light, dark, and high-contrast variants.
|
|
37
37
|
- `styles/` contains Material Design tokens: typography, shapes, elevation, motion, state layers.
|
|
38
38
|
- `foundations/` contains the responsive layout system (breakpoints: compact ≤599 px, medium 600–839 px, expanded 840–1199 px, large 1200–1599 px, extra-large ≥1600 px).
|
|
39
39
|
|
|
40
|
+
### Coding
|
|
41
|
+
|
|
42
|
+
- The amount of TypeScript code should be minimized. Necessary functionality should be solved using SCSS only.
|
|
43
|
+
– The SCSS code can be verbose, but the compiled CSS code must be as small as possible.
|
|
44
|
+
|
|
45
|
+
### Documentation
|
|
46
|
+
|
|
47
|
+
- Component documentation is stored with the SCSS and JavaScript files. Always check if the documentation is up to date with regards to the implementation.
|
|
48
|
+
|
|
40
49
|
### Build
|
|
41
50
|
|
|
42
51
|
`webpack.config.js` produces two identical outputs: one to `/dist/` (npm distribution) and one to `/docs/` (live demo). TypeScript target is ES2022 with strict mode and declaration-file generation enabled.
|
|
52
|
+
|
|
53
|
+
The library compiles to a UMD bundle (`dist/micl.js`) and a CSS file (`dist/micl.css`) with zero runtime dependencies. In addition, a CSS and JavaScript pair of compiled files is created, so that a consumer may load only a subset of components.
|
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
@use '../../styles/shapes';
|
|
24
24
|
@use '../../styles/typography';
|
|
25
25
|
|
|
26
|
+
@include shapes.corner('small');
|
|
27
|
+
|
|
28
|
+
@include typography.scale('title-medium');
|
|
29
|
+
@include typography.scale('body-medium');
|
|
30
|
+
|
|
26
31
|
:root {
|
|
27
32
|
--md-sys-alert-padding: 16px;
|
|
28
33
|
--md-sys-alert-space: 16px;
|
|
@@ -26,6 +26,18 @@
|
|
|
26
26
|
@use '../../styles/statelayer';
|
|
27
27
|
@use '../../styles/typography';
|
|
28
28
|
|
|
29
|
+
@include elevation.level(0);
|
|
30
|
+
@include elevation.level(2);
|
|
31
|
+
|
|
32
|
+
@include shapes.corner('none');
|
|
33
|
+
|
|
34
|
+
@include typography.scale('display-small');
|
|
35
|
+
@include typography.scale('headline-medium');
|
|
36
|
+
@include typography.scale('title-large');
|
|
37
|
+
@include typography.scale('title-medium');
|
|
38
|
+
@include typography.scale('title-small');
|
|
39
|
+
@include typography.scale('label-medium');
|
|
40
|
+
|
|
29
41
|
@mixin appbar-sticky() {
|
|
30
42
|
position: sticky;
|
|
31
43
|
inset: unset;
|
|
@@ -25,6 +25,15 @@
|
|
|
25
25
|
@use '../../styles/shapes';
|
|
26
26
|
@use '../../styles/statelayer';
|
|
27
27
|
|
|
28
|
+
@include elevation.level(1);
|
|
29
|
+
|
|
30
|
+
@include shapes.corner('extra-large');
|
|
31
|
+
@include shapes.corner('small');
|
|
32
|
+
|
|
33
|
+
@include statelayer.token('focus-indicator-thickness');
|
|
34
|
+
@include statelayer.token('focus-indicator-outer-offset');
|
|
35
|
+
@include statelayer.token('backdrop-opacity');
|
|
36
|
+
|
|
28
37
|
dialog.micl-bottomsheet {
|
|
29
38
|
--md-sys-bottomsheet-height: max-content;
|
|
30
39
|
--md-sys-bottomsheet-margin: 56px;
|
|
@@ -26,6 +26,29 @@
|
|
|
26
26
|
@use '../../styles/statelayer';
|
|
27
27
|
@use '../../styles/typography';
|
|
28
28
|
|
|
29
|
+
@include elevation.level(0);
|
|
30
|
+
@include elevation.level(1);
|
|
31
|
+
|
|
32
|
+
@include shapes.corner('small');
|
|
33
|
+
@include shapes.corner('medium');
|
|
34
|
+
@include shapes.corner('large');
|
|
35
|
+
@include shapes.corner('extra-large');
|
|
36
|
+
|
|
37
|
+
@include statelayer.token('hover-state-layer-opacity');
|
|
38
|
+
@include statelayer.token('focus-state-layer-opacity');
|
|
39
|
+
@include statelayer.token('pressed-state-layer-opacity');
|
|
40
|
+
@include statelayer.token('disabled-state-layer-opacity');
|
|
41
|
+
@include statelayer.token('focus-indicator-thickness');
|
|
42
|
+
@include statelayer.token('ripple-opacity-factor');
|
|
43
|
+
@include statelayer.token('ripple-duration');
|
|
44
|
+
@include statelayer.property;
|
|
45
|
+
@include statelayer.keyframes;
|
|
46
|
+
|
|
47
|
+
@include typography.scale('headline-large');
|
|
48
|
+
@include typography.scale('headline-small');
|
|
49
|
+
@include typography.scale('title-medium');
|
|
50
|
+
@include typography.scale('label-large');
|
|
51
|
+
|
|
29
52
|
button.micl-button-text-xs,
|
|
30
53
|
a.micl-button-text-xs,
|
|
31
54
|
button.micl-button-text-s,
|
|
@@ -27,6 +27,29 @@
|
|
|
27
27
|
@use '../../styles/statelayer';
|
|
28
28
|
@use '../../styles/typography';
|
|
29
29
|
|
|
30
|
+
@include elevation.level(0);
|
|
31
|
+
@include elevation.level(1);
|
|
32
|
+
@include elevation.level(2);
|
|
33
|
+
@include elevation.level(3);
|
|
34
|
+
@include elevation.level(4);
|
|
35
|
+
|
|
36
|
+
@include shapes.corner('medium');
|
|
37
|
+
|
|
38
|
+
@include statelayer.token('hover-state-layer-opacity');
|
|
39
|
+
@include statelayer.token('focus-state-layer-opacity');
|
|
40
|
+
@include statelayer.token('pressed-state-layer-opacity');
|
|
41
|
+
@include statelayer.token('dragged-state-layer-opacity');
|
|
42
|
+
@include statelayer.token('disabled-state-layer-opacity');
|
|
43
|
+
@include statelayer.token('focus-indicator-thickness');
|
|
44
|
+
@include statelayer.token('focus-indicator-outer-offset');
|
|
45
|
+
@include statelayer.token('ripple-opacity-factor');
|
|
46
|
+
@include statelayer.token('ripple-duration');
|
|
47
|
+
@include statelayer.property;
|
|
48
|
+
@include statelayer.keyframes;
|
|
49
|
+
|
|
50
|
+
@include typography.scale('title-medium');
|
|
51
|
+
@include typography.scale('body-smallmedium');
|
|
52
|
+
|
|
30
53
|
@mixin card-variant(
|
|
31
54
|
$name,
|
|
32
55
|
$bg-default,
|
|
@@ -21,9 +21,26 @@
|
|
|
21
21
|
|
|
22
22
|
@use '../../foundations';
|
|
23
23
|
@use '../../styles/motion';
|
|
24
|
+
@use '../../styles/shapes';
|
|
24
25
|
@use '../../styles/statelayer';
|
|
25
26
|
@use '../../styles/typography';
|
|
26
27
|
|
|
28
|
+
@include shapes.corner('full');
|
|
29
|
+
|
|
30
|
+
@include statelayer.token('layer-size');
|
|
31
|
+
@include statelayer.token('hover-state-layer-opacity');
|
|
32
|
+
@include statelayer.token('focus-state-layer-opacity');
|
|
33
|
+
@include statelayer.token('pressed-state-layer-opacity');
|
|
34
|
+
@include statelayer.token('disabled-state-layer-opacity');
|
|
35
|
+
@include statelayer.token('focus-indicator-thickness');
|
|
36
|
+
@include statelayer.token('ripple-opacity-factor');
|
|
37
|
+
@include statelayer.token('ripple-duration');
|
|
38
|
+
@include statelayer.property;
|
|
39
|
+
@include statelayer.keyframes;
|
|
40
|
+
|
|
41
|
+
@include motion.duration('short3');
|
|
42
|
+
@include motion.duration('long4');
|
|
43
|
+
|
|
27
44
|
:root {
|
|
28
45
|
--md-sys-checkbox-border-width: 2px;
|
|
29
46
|
--md-sys-checkbox-check-thickness: 2px;
|
|
@@ -25,6 +25,19 @@
|
|
|
25
25
|
@use '../../styles/statelayer';
|
|
26
26
|
@use '../../styles/typography';
|
|
27
27
|
|
|
28
|
+
@include shapes.corner('large');
|
|
29
|
+
@include shapes.corner('full');
|
|
30
|
+
|
|
31
|
+
@include statelayer.token('hover-state-layer-opacity');
|
|
32
|
+
@include statelayer.token('focus-state-layer-opacity');
|
|
33
|
+
@include statelayer.token('pressed-state-layer-opacity');
|
|
34
|
+
@include statelayer.token('disabled-state-layer-opacity');
|
|
35
|
+
@include statelayer.property;
|
|
36
|
+
|
|
37
|
+
@include typography.scale('headline-large');
|
|
38
|
+
@include typography.scale('body-large');
|
|
39
|
+
@include typography.scale('label-large');
|
|
40
|
+
|
|
28
41
|
:root {
|
|
29
42
|
--md-comp-date-picker-docked-container-width: 360px;
|
|
30
43
|
--md-comp-date-picker-modal-container-width: 360px;
|
|
@@ -26,6 +26,22 @@
|
|
|
26
26
|
@use '../../styles/statelayer';
|
|
27
27
|
@use '../../styles/typography';
|
|
28
28
|
|
|
29
|
+
@include elevation.level(0);
|
|
30
|
+
@include elevation.level(2);
|
|
31
|
+
@include elevation.level(3);
|
|
32
|
+
|
|
33
|
+
@include shapes.corner('none');
|
|
34
|
+
@include shapes.corner('extra-large');
|
|
35
|
+
|
|
36
|
+
@include statelayer.token('hover-state-layer-opacity');
|
|
37
|
+
@include statelayer.token('backdrop-opacity');
|
|
38
|
+
@include statelayer.property;
|
|
39
|
+
|
|
40
|
+
@include typography.scale('headline-small');
|
|
41
|
+
@include typography.scale('title-large');
|
|
42
|
+
@include typography.scale('title-medium');
|
|
43
|
+
@include typography.scale('body-medium');
|
|
44
|
+
|
|
29
45
|
:root {
|
|
30
46
|
--md-sys-dialog-min-width: 280px;
|
|
31
47
|
--md-sys-dialog-max-width: 560px;
|
|
@@ -25,6 +25,24 @@
|
|
|
25
25
|
@use '../../styles/shapes';
|
|
26
26
|
@use '../../styles/statelayer';
|
|
27
27
|
|
|
28
|
+
@include elevation.level(0);
|
|
29
|
+
|
|
30
|
+
@include shapes.corner('small');
|
|
31
|
+
@include shapes.corner('medium');
|
|
32
|
+
@include shapes.corner('large');
|
|
33
|
+
@include shapes.corner('extra-large');
|
|
34
|
+
@include shapes.corner('full');
|
|
35
|
+
|
|
36
|
+
@include statelayer.token('hover-state-layer-opacity');
|
|
37
|
+
@include statelayer.token('focus-state-layer-opacity');
|
|
38
|
+
@include statelayer.token('pressed-state-layer-opacity');
|
|
39
|
+
@include statelayer.token('disabled-state-layer-opacity');
|
|
40
|
+
@include statelayer.token('focus-indicator-thickness');
|
|
41
|
+
@include statelayer.token('ripple-opacity-factor');
|
|
42
|
+
@include statelayer.token('ripple-duration');
|
|
43
|
+
@include statelayer.property;
|
|
44
|
+
@include statelayer.keyframes;
|
|
45
|
+
|
|
28
46
|
button.micl-iconbutton-standard-xs,
|
|
29
47
|
a.micl-iconbutton-standard-xs,
|
|
30
48
|
button.micl-iconbutton-standard-s,
|
|
@@ -26,6 +26,31 @@
|
|
|
26
26
|
@use '../../styles/statelayer';
|
|
27
27
|
@use '../../styles/typography';
|
|
28
28
|
|
|
29
|
+
@include elevation.level(4);
|
|
30
|
+
|
|
31
|
+
@include shapes.corner('extra-small');
|
|
32
|
+
@include shapes.corner('small');
|
|
33
|
+
@include shapes.corner('medium');
|
|
34
|
+
@include shapes.corner('large');
|
|
35
|
+
@include shapes.corner('full');
|
|
36
|
+
|
|
37
|
+
@include statelayer.token('hover-state-layer-opacity');
|
|
38
|
+
@include statelayer.token('focus-state-layer-opacity');
|
|
39
|
+
@include statelayer.token('pressed-state-layer-opacity');
|
|
40
|
+
@include statelayer.token('dragged-state-layer-opacity');
|
|
41
|
+
@include statelayer.token('disabled-state-layer-opacity');
|
|
42
|
+
@include statelayer.token('focus-indicator-thickness');
|
|
43
|
+
@include statelayer.token('focus-indicator-inner-offset');
|
|
44
|
+
@include statelayer.token('ripple-opacity-factor');
|
|
45
|
+
@include statelayer.token('ripple-duration');
|
|
46
|
+
@include statelayer.property;
|
|
47
|
+
@include statelayer.keyframes;
|
|
48
|
+
|
|
49
|
+
@include typography.scale('title-medium');
|
|
50
|
+
@include typography.scale('body-large');
|
|
51
|
+
@include typography.scale('body-medium');
|
|
52
|
+
@include typography.scale('label-small');
|
|
53
|
+
|
|
29
54
|
:root {
|
|
30
55
|
--md-comp-accordion-item-space: 0px;
|
|
31
56
|
}
|
|
@@ -23,8 +23,26 @@
|
|
|
23
23
|
@use '../../styles/elevation';
|
|
24
24
|
@use '../../styles/motion';
|
|
25
25
|
@use '../../styles/shapes';
|
|
26
|
+
@use '../../styles/statelayer';
|
|
26
27
|
@use '../../styles/typography';
|
|
27
28
|
|
|
29
|
+
@include elevation.level(2);
|
|
30
|
+
|
|
31
|
+
@include shapes.corner('small');
|
|
32
|
+
@include shapes.corner('large');
|
|
33
|
+
|
|
34
|
+
@include statelayer.token('hover-state-layer-opacity');
|
|
35
|
+
@include statelayer.token('focus-state-layer-opacity');
|
|
36
|
+
@include statelayer.token('pressed-state-layer-opacity');
|
|
37
|
+
@include statelayer.token('disabled-state-layer-opacity');
|
|
38
|
+
@include statelayer.token('focus-indicator-thickness');
|
|
39
|
+
@include statelayer.token('focus-indicator-inner-offset');
|
|
40
|
+
@include statelayer.token('backdrop-opacity');
|
|
41
|
+
|
|
42
|
+
@include typography.scale('label-large');
|
|
43
|
+
@include typography.scale('label-small');
|
|
44
|
+
@include typography.scale('body-small');
|
|
45
|
+
|
|
28
46
|
:root {
|
|
29
47
|
--md-comp-menu-width-max: 320px;
|
|
30
48
|
--md-comp-menu-width-min: 112px;
|
|
@@ -27,6 +27,23 @@
|
|
|
27
27
|
@use '../../styles/statelayer';
|
|
28
28
|
@use '../../styles/typography';
|
|
29
29
|
|
|
30
|
+
@include elevation.level(0);
|
|
31
|
+
@include elevation.level(2);
|
|
32
|
+
|
|
33
|
+
@include shapes.corner('large');
|
|
34
|
+
|
|
35
|
+
@include statelayer.token('hover-state-layer-opacity');
|
|
36
|
+
@include statelayer.token('focus-state-layer-opacity');
|
|
37
|
+
@include statelayer.token('pressed-state-layer-opacity');
|
|
38
|
+
@include statelayer.token('backdrop-opacity');
|
|
39
|
+
@include statelayer.token('ripple-opacity-factor');
|
|
40
|
+
@include statelayer.token('ripple-duration');
|
|
41
|
+
@include statelayer.property;
|
|
42
|
+
@include statelayer.keyframes;
|
|
43
|
+
|
|
44
|
+
@include typography.scale('label-large');
|
|
45
|
+
@include typography.scale('label-medium');
|
|
46
|
+
|
|
30
47
|
.micl-navigationrail {
|
|
31
48
|
--md-comp-nav-rail-spring-buffer: 100px;
|
|
32
49
|
--md-comp-nav-rail-divider-thickness: 0px;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Progress Indicator
|
|
2
|
+
This component implements the [Material Design 3 Expressive Progress indicators](https://m3.material.io/components/progress-indicators/overview) design. Progress indicators show the status of a process in real time, either as a determinate value or as an indeterminate, looping animation.
|
|
3
|
+
|
|
4
|
+
## Basic Usage
|
|
5
|
+
|
|
6
|
+
### HTML
|
|
7
|
+
Progress indicators use the native `<progress>` element. Add the `micl-progress-linear` class for the linear (bar) variant or `micl-progress-circular` for the circular (ring) variant.
|
|
8
|
+
|
|
9
|
+
A **determinate** indicator has a known value. Provide the `value` and (optionally) `max` attributes:
|
|
10
|
+
|
|
11
|
+
```HTML
|
|
12
|
+
<progress class="micl-progress-linear" value="0.6" max="1"></progress>
|
|
13
|
+
<progress class="micl-progress-circular" value="60" max="100"></progress>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
An **indeterminate** indicator has an unknown value. Omit the `value` attribute entirely:
|
|
17
|
+
|
|
18
|
+
```HTML
|
|
19
|
+
<progress class="micl-progress-linear"></progress>
|
|
20
|
+
<progress class="micl-progress-circular"></progress>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### CSS
|
|
24
|
+
Import the progress indicator styles into your project:
|
|
25
|
+
|
|
26
|
+
```CSS
|
|
27
|
+
@use "material-inspired-component-library/dist/progressindicator";
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or import all MICL styles:
|
|
31
|
+
```CSS
|
|
32
|
+
@use "material-inspired-component-library/styles";
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### JavaScript
|
|
36
|
+
No custom JavaScript is required for indeterminate indicators — the looping animation is driven entirely by CSS via the native `:indeterminate` state.
|
|
37
|
+
|
|
38
|
+
For determinate indicators, the bundled handler mirrors the `value` and `max` attributes into the CSS custom properties that drive the fill, the active/track gap and the Expressive wave amplitude. It also flattens the wave into a straight line over the final 10% of progress.
|
|
39
|
+
|
|
40
|
+
> **Updating progress at runtime:** the handler observes the `value` and `max` *attributes*. When you update progress dynamically, set the attribute (`element.setAttribute('value', 0.7)`) rather than only the IDL property (`element.value = 0.7`), so the indicator stays in sync. Alternatively, you may set the `--md-comp-progress-fraction` custom property (a number between `0` and `1`) directly for pure-CSS control without the handler.
|
|
41
|
+
|
|
42
|
+
### Live Demo
|
|
43
|
+
A live example of the [Progress Indicator component](https://henkpb.github.io/micl/progressindicator.html) is available to interact with.
|
|
44
|
+
|
|
45
|
+
## Variants
|
|
46
|
+
The Progress Indicator component offers the following variants:
|
|
47
|
+
|
|
48
|
+
| CSS class | Description |
|
|
49
|
+
| --------- | ----------- |
|
|
50
|
+
| micl-progress-linear | A horizontal linear progress bar |
|
|
51
|
+
| micl-progress-circular | A circular progress ring |
|
|
52
|
+
| micl-progress-circular--s | Small circular ring (28px) |
|
|
53
|
+
| micl-progress-circular--m | Medium circular ring (48px, default) |
|
|
54
|
+
| micl-progress-circular--l | Large circular ring (64px) |
|
|
55
|
+
|
|
56
|
+
Each variant is **determinate** when a `value` attribute is present and **indeterminate** when it is omitted.
|
|
57
|
+
|
|
58
|
+
The signature Expressive *wavy active indicator* is applied to the linear variant as a static shape (rasterised once, so any number of determinate indicators cost nothing per frame). Its amplitude eases to zero as the indicator reaches completion, and setting `--md-comp-progress-wave-amplitude` to `0` produces a flat (pre-Expressive) active indicator. Only the transient loading states animate: the linear indeterminate comet, and the circular indeterminate spinner (a GPU-composited rotation). The circular variant renders a smooth ring.
|
|
59
|
+
|
|
60
|
+
The component respects the `dir` global attribute, automatically mirroring the linear sweep direction for right-to-left (RTL) languages when `dir="rtl"` is applied to an ancestor element. It also honours the user's `prefers-reduced-motion` setting by disabling the travelling wave and looping animations.
|
|
61
|
+
|
|
62
|
+
## Customizations
|
|
63
|
+
You can customize the appearance of the Progress Indicator component by overriding its global CSS variables. Following the MICL convention, these `--md-comp-progress-*` variables can be set on `:root` or on any appropriate parent element to affect its child progress indicators. The colour roles default to the Material Design system colour tokens per the M3 progress-indicator specification.
|
|
64
|
+
|
|
65
|
+
| Variable name | Default Value | Description |
|
|
66
|
+
| ------------- | ------------- | ----------- |
|
|
67
|
+
| --md-comp-progress-active-color | var(--md-sys-color-primary) | Colour of the active (filled) indicator |
|
|
68
|
+
| --md-comp-progress-track-color | var(--md-sys-color-secondary-container) | Colour of the remaining track |
|
|
69
|
+
| --md-comp-progress-stop-color | var(--md-sys-color-primary) | Colour of the linear end stop indicator dot |
|
|
70
|
+
| --md-comp-progress-thickness | 4px | Thickness of the linear track and circular ring stroke |
|
|
71
|
+
| --md-comp-progress-track-gap | 4px | Gap between the active indicator and the remaining track (linear) |
|
|
72
|
+
| --md-comp-progress-stop-size | 4px | Diameter of the linear end stop indicator dot |
|
|
73
|
+
| --md-comp-progress-linear-width | 100% | Default width of the linear indicator |
|
|
74
|
+
| --md-comp-progress-wave-wavelength | 40px | Wavelength of the Expressive active wave |
|
|
75
|
+
| --md-comp-progress-wave-amplitude | 3px | Amplitude of the Expressive active wave (set to `0` to disable) |
|
|
76
|
+
| --md-comp-progress-indeterminate-duration | var(--md-sys-motion-duration-extra-long4) | Period of the indeterminate loop |
|
|
77
|
+
| --md-comp-progress-circular-size | 48px | Diameter of the circular indicator |
|
|
78
|
+
| --md-comp-progress-wave-image | (inline SVG) | The sine-ribbon mask used for the wavy active indicator |
|
|
79
|
+
|
|
80
|
+
**Example: A thicker linear indicator with a calmer wave**
|
|
81
|
+
|
|
82
|
+
```HTML
|
|
83
|
+
<progress class="micl-progress-linear" value="0.4"
|
|
84
|
+
style="--md-comp-progress-thickness:8px;--md-comp-progress-wave-amplitude:2px"></progress>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Compatibility
|
|
88
|
+
This component utilizes relative RGB color values, CSS `mask`, `conic-gradient` and registered `@property` custom properties, which may not be fully supported in your browser. Please check [Browser compatibility](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#browser_compatibility) for details.
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright © 2025 Hermana AS
|
|
3
|
+
//
|
|
4
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
// of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
// in the Software without restriction, including without limitation the rights
|
|
7
|
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
// copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
// furnished to do so, subject to the following conditions:
|
|
10
|
+
//
|
|
11
|
+
// The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
// copies or substantial portions of the Software.
|
|
13
|
+
//
|
|
14
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
// SOFTWARE.
|
|
21
|
+
|
|
22
|
+
@use '../../foundations';
|
|
23
|
+
@use '../../styles/motion';
|
|
24
|
+
|
|
25
|
+
@include motion.duration('long2');
|
|
26
|
+
@include motion.duration('extra-long4');
|
|
27
|
+
|
|
28
|
+
// Registered so determinate updates from the JS handler interpolate smoothly.
|
|
29
|
+
// Never keyframe-animated.
|
|
30
|
+
@property --md-comp-progress-fraction {
|
|
31
|
+
syntax: '<number>';
|
|
32
|
+
initial-value: 0;
|
|
33
|
+
inherits: true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Non-themed defaults. Declared on :root so they can be overridden on :root or
|
|
37
|
+
// any parent element (consistent with the other MICL components). The colour
|
|
38
|
+
// roles are resolved per the M3 progress-indicator spec at the point of use
|
|
39
|
+
// (see $active/$track/$stop) because the theme palette is scoped to the
|
|
40
|
+
// .light/.dark window class and is therefore NOT in scope at :root.
|
|
41
|
+
:root {
|
|
42
|
+
--md-comp-progress-thickness: 4px;
|
|
43
|
+
--md-comp-progress-track-gap: 4px;
|
|
44
|
+
--md-comp-progress-stop-size: 4px;
|
|
45
|
+
--md-comp-progress-linear-width: 100%;
|
|
46
|
+
--md-comp-progress-wave-wavelength: 40px;
|
|
47
|
+
--md-comp-progress-wave-amplitude: 3px;
|
|
48
|
+
--md-comp-progress-indeterminate-duration: var(--md-sys-motion-duration-extra-long4);
|
|
49
|
+
--md-comp-progress-circular-size: 48px;
|
|
50
|
+
--md-comp-progress-amplitude-scale: 1;
|
|
51
|
+
|
|
52
|
+
// One wavelength of a sine ribbon, stroked at a constant width, used as a
|
|
53
|
+
// static mask so the active indicator takes a wavy shape. Painted once —
|
|
54
|
+
// no animation, no per-frame cost. The stroke is WHITE so it reveals
|
|
55
|
+
// correctly whether the browser treats the SVG mask as alpha (white =
|
|
56
|
+
// opaque) or luminance (white = 1).
|
|
57
|
+
--md-comp-progress-wave-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 10' preserveAspectRatio='none'%3E%3Cpath d='M0 5 Q10 -1 20 5 T40 5' fill='none' stroke='white' stroke-width='4' stroke-linecap='round'/%3E%3C/svg%3E");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// M3 progress-indicator colour mapping. Resolved on the component element so
|
|
61
|
+
// the system palette (scoped to the themed window ancestor) is in scope; a
|
|
62
|
+
// consumer-supplied --md-comp-progress-*-color on any ancestor still wins.
|
|
63
|
+
$active: var(--md-comp-progress-active-color, var(--md-sys-color-primary));
|
|
64
|
+
$track: var(--md-comp-progress-track-color, var(--md-sys-color-secondary-container));
|
|
65
|
+
$stop: var(--md-comp-progress-stop-color, var(--md-sys-color-primary));
|
|
66
|
+
|
|
67
|
+
%progress-reset {
|
|
68
|
+
-webkit-appearance: none;
|
|
69
|
+
appearance: none;
|
|
70
|
+
border: none;
|
|
71
|
+
margin: 0;
|
|
72
|
+
background-color: transparent;
|
|
73
|
+
color: $active;
|
|
74
|
+
vertical-align: middle;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Static wavy active indicator. No animation: the mask is rasterised once and
|
|
78
|
+
// never sampled again, so a screen full of determinate indicators costs
|
|
79
|
+
// nothing per frame.
|
|
80
|
+
@mixin wave {
|
|
81
|
+
background-color: $active;
|
|
82
|
+
-webkit-mask-image: var(--md-comp-progress-wave-image);
|
|
83
|
+
mask-image: var(--md-comp-progress-wave-image);
|
|
84
|
+
-webkit-mask-repeat: repeat-x;
|
|
85
|
+
mask-repeat: repeat-x;
|
|
86
|
+
-webkit-mask-position: 0 center;
|
|
87
|
+
mask-position: 0 center;
|
|
88
|
+
-webkit-mask-size: var(--md-comp-progress-wave-wavelength)
|
|
89
|
+
calc(var(--md-comp-progress-thickness) + 2 * var(--md-comp-progress-wave-amplitude) * var(--md-comp-progress-amplitude-scale, 1));
|
|
90
|
+
mask-size: var(--md-comp-progress-wave-wavelength)
|
|
91
|
+
calc(var(--md-comp-progress-thickness) + 2 * var(--md-comp-progress-wave-amplitude) * var(--md-comp-progress-amplitude-scale, 1));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
//
|
|
95
|
+
// Linear progress indicator
|
|
96
|
+
//
|
|
97
|
+
progress.micl-progress-linear {
|
|
98
|
+
@extend %progress-reset;
|
|
99
|
+
|
|
100
|
+
display: inline-block;
|
|
101
|
+
inline-size: var(--md-comp-progress-linear-width);
|
|
102
|
+
block-size: calc(var(--md-comp-progress-thickness) + 2 * var(--md-comp-progress-wave-amplitude));
|
|
103
|
+
|
|
104
|
+
// Track (remaining portion) + end stop indicator. Starts one track-gap
|
|
105
|
+
// after the active indicator, clipped to --thickness tall and centred.
|
|
106
|
+
@mixin track {
|
|
107
|
+
background-color: transparent;
|
|
108
|
+
background-image:
|
|
109
|
+
radial-gradient(circle, #{$stop} 0 50%, transparent 50%),
|
|
110
|
+
linear-gradient(90deg,
|
|
111
|
+
transparent 0 calc(var(--md-comp-progress-fraction, 0) * 100% + var(--md-comp-progress-track-gap)),
|
|
112
|
+
#{$track} calc(var(--md-comp-progress-fraction, 0) * 100% + var(--md-comp-progress-track-gap)) 100%);
|
|
113
|
+
background-repeat: no-repeat;
|
|
114
|
+
background-position: right center, left center;
|
|
115
|
+
background-size:
|
|
116
|
+
var(--md-comp-progress-stop-size) var(--md-comp-progress-stop-size),
|
|
117
|
+
100% var(--md-comp-progress-thickness);
|
|
118
|
+
border-radius: calc(var(--md-comp-progress-thickness) / 2);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
&::-webkit-progress-bar { @include track; }
|
|
122
|
+
&::-webkit-progress-value { @include wave; }
|
|
123
|
+
&::-moz-progress-bar { @include wave; }
|
|
124
|
+
|
|
125
|
+
// Firefox renders the track on the element itself.
|
|
126
|
+
@-moz-document url-prefix() {
|
|
127
|
+
@include track;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Indeterminate: a comet sweeping a transparent track. Only this transient
|
|
131
|
+
// state animates; the move is a single small repainted strip.
|
|
132
|
+
&:indeterminate {
|
|
133
|
+
background-image:
|
|
134
|
+
linear-gradient(90deg, transparent, #{$active}, transparent),
|
|
135
|
+
linear-gradient(#{$track}, #{$track});
|
|
136
|
+
background-repeat: no-repeat;
|
|
137
|
+
background-position: -40% center, left center;
|
|
138
|
+
background-size: 40% var(--md-comp-progress-thickness), 100% var(--md-comp-progress-thickness);
|
|
139
|
+
border-radius: calc(var(--md-comp-progress-thickness) / 2);
|
|
140
|
+
animation: micl-progress-linear-indeterminate var(--md-comp-progress-indeterminate-duration) var(--md-sys-motion-easing-standard, ease-in-out) infinite;
|
|
141
|
+
|
|
142
|
+
&::-webkit-progress-bar,
|
|
143
|
+
&::-webkit-progress-value { background: transparent; }
|
|
144
|
+
&::-moz-progress-bar { background: transparent; }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@keyframes micl-progress-linear-indeterminate {
|
|
149
|
+
0% { background-position: -40% center, left center; }
|
|
150
|
+
100% { background-position: 140% center, left center; }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
//
|
|
154
|
+
// Circular progress indicator
|
|
155
|
+
//
|
|
156
|
+
// Determinate: a static conic-gradient ring punched by a radial mask (white =
|
|
157
|
+
// kept under both alpha and luminance masking). It is painted once and never
|
|
158
|
+
// animated, so it costs nothing per frame.
|
|
159
|
+
//
|
|
160
|
+
progress.micl-progress-circular {
|
|
161
|
+
@extend %progress-reset;
|
|
162
|
+
|
|
163
|
+
--md-comp-progress-circular-gap-deg: 12deg;
|
|
164
|
+
|
|
165
|
+
display: inline-block;
|
|
166
|
+
box-sizing: border-box;
|
|
167
|
+
inline-size: var(--md-comp-progress-circular-size);
|
|
168
|
+
block-size: var(--md-comp-progress-circular-size);
|
|
169
|
+
border-radius: 50%;
|
|
170
|
+
background-image: conic-gradient(
|
|
171
|
+
#{$active} 0
|
|
172
|
+
max(0deg, calc(var(--md-comp-progress-fraction, 0) * 360deg - var(--md-comp-progress-circular-gap-deg))),
|
|
173
|
+
transparent
|
|
174
|
+
max(0deg, calc(var(--md-comp-progress-fraction, 0) * 360deg - var(--md-comp-progress-circular-gap-deg)))
|
|
175
|
+
calc(var(--md-comp-progress-fraction, 0) * 360deg),
|
|
176
|
+
#{$track} calc(var(--md-comp-progress-fraction, 0) * 360deg) 360deg);
|
|
177
|
+
-webkit-mask: radial-gradient(farthest-side,
|
|
178
|
+
transparent calc(100% - var(--md-comp-progress-thickness)),
|
|
179
|
+
#fff calc(100% - var(--md-comp-progress-thickness)));
|
|
180
|
+
mask: radial-gradient(farthest-side,
|
|
181
|
+
transparent calc(100% - var(--md-comp-progress-thickness)),
|
|
182
|
+
#fff calc(100% - var(--md-comp-progress-thickness)));
|
|
183
|
+
|
|
184
|
+
&::-webkit-progress-bar,
|
|
185
|
+
&::-webkit-progress-value { background: transparent; }
|
|
186
|
+
&::-moz-progress-bar { background: transparent; }
|
|
187
|
+
|
|
188
|
+
&.micl-progress-circular--s { --md-comp-progress-circular-size: 28px; }
|
|
189
|
+
&.micl-progress-circular--m { --md-comp-progress-circular-size: 48px; }
|
|
190
|
+
&.micl-progress-circular--l { --md-comp-progress-circular-size: 64px; }
|
|
191
|
+
|
|
192
|
+
// Indeterminate: the canonical lightweight CSS spinner — a plain bordered
|
|
193
|
+
// ring (no gradient, no mask) rotated by transform. Chrome promotes this to
|
|
194
|
+
// a compositor layer, rasterises it once, and spins the texture on the GPU,
|
|
195
|
+
// so it is essentially free per frame regardless of how many are visible.
|
|
196
|
+
&:indeterminate {
|
|
197
|
+
background-image: none;
|
|
198
|
+
-webkit-mask: none;
|
|
199
|
+
mask: none;
|
|
200
|
+
border: var(--md-comp-progress-thickness) solid #{$track};
|
|
201
|
+
border-block-start-color: $active;
|
|
202
|
+
animation: micl-progress-circular-spin var(--md-comp-progress-indeterminate-duration) linear infinite;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@keyframes micl-progress-circular-spin {
|
|
207
|
+
to { transform: rotate(360deg); }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
//
|
|
211
|
+
// Right-to-left: mirror the linear sweep direction.
|
|
212
|
+
//
|
|
213
|
+
[dir=rtl] progress.micl-progress-linear {
|
|
214
|
+
transform: scaleX(-1);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
//
|
|
218
|
+
// Honour reduced-motion: stop the only two animated (loading) states.
|
|
219
|
+
//
|
|
220
|
+
@media (prefers-reduced-motion: reduce) {
|
|
221
|
+
progress.micl-progress-linear:indeterminate,
|
|
222
|
+
progress.micl-progress-circular:indeterminate {
|
|
223
|
+
animation: none;
|
|
224
|
+
}
|
|
225
|
+
}
|