@eagami/ui 4.5.1 → 4.7.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 +10 -2
- package/fesm2022/eagami-ui.mjs +155 -93
- package/fesm2022/eagami-ui.mjs.map +1 -1
- package/package.json +5 -2
- package/schematics/collection.json +9 -0
- package/schematics/ng-add/index.js +64 -0
- package/schematics/ng-add/schema.js +2 -0
- package/schematics/ng-add/schema.json +16 -0
- package/schematics/ng-add/utils.js +53 -0
- package/schematics/package.json +3 -0
- package/src/styles/_mixins.scss +68 -0
- package/src/styles/_reset.scss +62 -0
- package/src/styles/_rtl.scss +38 -0
- package/src/styles/_tooltip.scss +39 -0
- package/src/styles/eagami-ui.scss +7 -0
- package/src/styles/tokens/_colors.scss +265 -0
- package/src/styles/tokens/_elevation.scss +87 -0
- package/src/styles/tokens/_index.scss +6 -0
- package/src/styles/tokens/_motion.scss +37 -0
- package/src/styles/tokens/_shape.scss +23 -0
- package/src/styles/tokens/_spacing.scss +73 -0
- package/src/styles/tokens/_typography.scss +147 -0
- package/types/eagami-ui.d.ts +7 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eagami/ui",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.0",
|
|
4
4
|
"description": "Lightweight, accessible, themeable Angular UI component library and icon set built on CSS custom properties",
|
|
5
5
|
"author": "Michal Wiraszka <michal@eagami.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -55,6 +55,9 @@
|
|
|
55
55
|
"files": [
|
|
56
56
|
"fesm2022",
|
|
57
57
|
"types",
|
|
58
|
+
"src/styles",
|
|
59
|
+
"schematics",
|
|
58
60
|
"README.md"
|
|
59
|
-
]
|
|
61
|
+
],
|
|
62
|
+
"schematics": "./schematics/collection.json"
|
|
60
63
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ngAdd = ngAdd;
|
|
4
|
+
const schematics_1 = require("@angular-devkit/schematics");
|
|
5
|
+
const workspace_1 = require("@schematics/angular/utility/workspace");
|
|
6
|
+
const utils_1 = require("./utils");
|
|
7
|
+
function ngAdd(options) {
|
|
8
|
+
return (0, schematics_1.chain)([registerStylesheet(options), registerFonts(options), logNextSteps()]);
|
|
9
|
+
}
|
|
10
|
+
function registerStylesheet(options) {
|
|
11
|
+
return (0, workspace_1.updateWorkspace)(workspace => {
|
|
12
|
+
const { project } = findProject(workspace, options.project);
|
|
13
|
+
const build = project.targets.get('build');
|
|
14
|
+
if (!build) {
|
|
15
|
+
throw new schematics_1.SchematicsException('No "build" target found; cannot register the Eagami UI stylesheet.');
|
|
16
|
+
}
|
|
17
|
+
build.options ??= {};
|
|
18
|
+
const existing = build.options['styles'];
|
|
19
|
+
const styles = Array.isArray(existing) ? existing : [];
|
|
20
|
+
build.options['styles'] = (0, utils_1.withEagamiStyle)(styles);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function registerFonts(options) {
|
|
24
|
+
return async (tree, context) => {
|
|
25
|
+
const workspace = await (0, workspace_1.getWorkspace)(tree);
|
|
26
|
+
const { name, project } = findProject(workspace, options.project);
|
|
27
|
+
const indexPath = (0, utils_1.resolveIndexPath)(project.targets.get('build')?.options?.['index']);
|
|
28
|
+
if (!indexPath) {
|
|
29
|
+
context.logger.warn(`No index.html found for project "${name}"; add the Eagami UI fonts manually.`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const current = tree.read(indexPath);
|
|
33
|
+
if (!current) {
|
|
34
|
+
context.logger.warn(`Could not read "${indexPath}"; add the Eagami UI fonts manually.`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const html = current.toString('utf-8');
|
|
38
|
+
const updated = (0, utils_1.withEagamiFonts)(html);
|
|
39
|
+
if (updated !== html) {
|
|
40
|
+
tree.overwrite(indexPath, updated);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function logNextSteps() {
|
|
45
|
+
return (_tree, context) => {
|
|
46
|
+
context.logger.info('Eagami UI is set up: the global stylesheet and fonts are registered.');
|
|
47
|
+
context.logger.info('Optional: call provideEagamiUi() in app.config.ts for a custom brand palette or extra locales.');
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function findProject(workspace, name) {
|
|
51
|
+
if (name) {
|
|
52
|
+
const project = workspace.projects.get(name);
|
|
53
|
+
if (!project) {
|
|
54
|
+
throw new schematics_1.SchematicsException(`Project "${name}" not found in the workspace.`);
|
|
55
|
+
}
|
|
56
|
+
return { name, project };
|
|
57
|
+
}
|
|
58
|
+
for (const [projectName, project] of workspace.projects) {
|
|
59
|
+
if (project.extensions['projectType'] === 'application') {
|
|
60
|
+
return { name: projectName, project };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
throw new schematics_1.SchematicsException('No application project found to set up Eagami UI.');
|
|
64
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"$id": "eagami-ui-ng-add",
|
|
4
|
+
"title": "Eagami UI ng-add schematic",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"project": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"description": "The project to set up Eagami UI in.",
|
|
10
|
+
"$default": {
|
|
11
|
+
"$source": "projectName"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"additionalProperties": false
|
|
16
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Pure, dependency-free transforms for the ng-add schematic. Kept separate from
|
|
3
|
+
// index.ts (which pulls in @angular-devkit) so the unit tests compile under the
|
|
4
|
+
// Angular test build without dragging Node-only schematics APIs into the bundle.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.EAGAMI_FONT_LINKS = exports.EAGAMI_STYLE_PATH = void 0;
|
|
7
|
+
exports.styleEntryMatches = styleEntryMatches;
|
|
8
|
+
exports.withEagamiStyle = withEagamiStyle;
|
|
9
|
+
exports.hasEagamiFonts = hasEagamiFonts;
|
|
10
|
+
exports.withEagamiFonts = withEagamiFonts;
|
|
11
|
+
exports.resolveIndexPath = resolveIndexPath;
|
|
12
|
+
exports.EAGAMI_STYLE_PATH = 'node_modules/@eagami/ui/src/styles/eagami-ui.scss';
|
|
13
|
+
exports.EAGAMI_FONT_LINKS = [
|
|
14
|
+
'<link rel="preconnect" href="https://fonts.googleapis.com" />',
|
|
15
|
+
'<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />',
|
|
16
|
+
'<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,400&family=Syne:wght@400;500;600;700&display=swap" />',
|
|
17
|
+
];
|
|
18
|
+
// Unique to our Google Fonts request, so it doubles as the idempotency marker
|
|
19
|
+
const FONT_MARKER = 'family=DM+Sans';
|
|
20
|
+
function styleEntryMatches(entry, path) {
|
|
21
|
+
return typeof entry === 'string' ? entry === path : entry.input === path;
|
|
22
|
+
}
|
|
23
|
+
function withEagamiStyle(styles) {
|
|
24
|
+
if (styles.some(style => styleEntryMatches(style, exports.EAGAMI_STYLE_PATH))) {
|
|
25
|
+
return styles;
|
|
26
|
+
}
|
|
27
|
+
return [exports.EAGAMI_STYLE_PATH, ...styles];
|
|
28
|
+
}
|
|
29
|
+
function hasEagamiFonts(html) {
|
|
30
|
+
return html.includes(FONT_MARKER);
|
|
31
|
+
}
|
|
32
|
+
function withEagamiFonts(html) {
|
|
33
|
+
if (hasEagamiFonts(html)) {
|
|
34
|
+
return html;
|
|
35
|
+
}
|
|
36
|
+
const block = exports.EAGAMI_FONT_LINKS.map(link => ` ${link}`).join('\n');
|
|
37
|
+
const headClose = /([ \t]*)<\/head>/i;
|
|
38
|
+
if (headClose.test(html)) {
|
|
39
|
+
return html.replace(headClose, `${block}\n$1</head>`);
|
|
40
|
+
}
|
|
41
|
+
return `${block}\n${html}`;
|
|
42
|
+
}
|
|
43
|
+
// The build target's `index` option is either a path string or a { input, output } object
|
|
44
|
+
function resolveIndexPath(index) {
|
|
45
|
+
if (typeof index === 'string') {
|
|
46
|
+
return index;
|
|
47
|
+
}
|
|
48
|
+
if (index !== null && typeof index === 'object' && 'input' in index) {
|
|
49
|
+
const input = index.input;
|
|
50
|
+
return typeof input === 'string' ? input : undefined;
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Shared SCSS mixins for the library. Consume from a component stylesheet with:
|
|
2
|
+
// @use '../../styles/mixins' as ea;
|
|
3
|
+
// .ea-foo__close { @include ea.icon-button; }
|
|
4
|
+
|
|
5
|
+
// Standard focus indicator. The ring is a soft box-shadow halo, but box-shadow
|
|
6
|
+
// is dropped in forced-colors (Windows High Contrast) mode, which would leave
|
|
7
|
+
// keyboard users with no visible focus. Pair the halo with a real outline that
|
|
8
|
+
// only renders in forced-colors so the ring survives there. Pass the full
|
|
9
|
+
// box-shadow value to compose the ring with an existing shadow.
|
|
10
|
+
@mixin focus-ring($ring: var(--shadow-focus-ring)) {
|
|
11
|
+
box-shadow: $ring;
|
|
12
|
+
|
|
13
|
+
@media (forced-colors: active) {
|
|
14
|
+
outline: 2px solid Highlight;
|
|
15
|
+
outline-offset: 2px;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Floating surfaces (menus, dialogs, popovers, toasts) lean on elevation shadow
|
|
20
|
+
// for their boundary, but shadows are dropped in forced-colors mode. Add a
|
|
21
|
+
// hairline border there so the surface stays separated from the content behind.
|
|
22
|
+
@mixin elevated-surface-border {
|
|
23
|
+
@media (forced-colors: active) {
|
|
24
|
+
border: 1px solid CanvasText;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Standard icon button: clear, close/dismiss, password-toggle, nav, etc. The box
|
|
29
|
+
// is `--ea-icon-button-size` (em) so it scales with the host component's size and
|
|
30
|
+
// is consistent across components at a given size tier. The glyph is enlarged so
|
|
31
|
+
// it reads clearly inside the box regardless of how much padding the icon's own
|
|
32
|
+
// viewBox carries (the feather `x`, for instance, only fills its middle half).
|
|
33
|
+
@mixin icon-button {
|
|
34
|
+
display: inline-flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
justify-content: center;
|
|
37
|
+
flex-shrink: 0;
|
|
38
|
+
width: var(--ea-icon-button-size, 1.75em);
|
|
39
|
+
height: var(--ea-icon-button-size, 1.75em);
|
|
40
|
+
padding: 0;
|
|
41
|
+
border: none;
|
|
42
|
+
border-radius: var(--radius-sm);
|
|
43
|
+
background: none;
|
|
44
|
+
color: var(--color-text-secondary);
|
|
45
|
+
cursor: pointer;
|
|
46
|
+
transition: var(--transition-colors);
|
|
47
|
+
|
|
48
|
+
// Glyph sized via font-size (not width) so it overrides the icon's inline 1em
|
|
49
|
+
// host size and still scales with the box.
|
|
50
|
+
> * {
|
|
51
|
+
font-size: 1.25em;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
&:hover {
|
|
55
|
+
background-color: var(--color-state-hover);
|
|
56
|
+
color: var(--color-text-primary);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
&:focus-visible {
|
|
60
|
+
outline: none;
|
|
61
|
+
@include focus-ring;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
&:disabled {
|
|
65
|
+
cursor: not-allowed;
|
|
66
|
+
opacity: 0.5;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
*,
|
|
2
|
+
*::before,
|
|
3
|
+
*::after {
|
|
4
|
+
box-sizing: border-box;
|
|
5
|
+
margin: 0;
|
|
6
|
+
padding: 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
html {
|
|
10
|
+
font-size: 16px;
|
|
11
|
+
-webkit-text-size-adjust: 100%;
|
|
12
|
+
tab-size: 4;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
body {
|
|
16
|
+
font-family: var(--font-family-sans);
|
|
17
|
+
font-size: var(--font-size-md);
|
|
18
|
+
line-height: var(--line-height-normal);
|
|
19
|
+
color: var(--color-text-primary);
|
|
20
|
+
background-color: var(--color-bg-canvas);
|
|
21
|
+
-webkit-font-smoothing: antialiased;
|
|
22
|
+
-moz-osx-font-smoothing: grayscale;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
img,
|
|
26
|
+
svg,
|
|
27
|
+
video {
|
|
28
|
+
display: block;
|
|
29
|
+
max-width: 100%;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
button,
|
|
33
|
+
input,
|
|
34
|
+
optgroup,
|
|
35
|
+
select,
|
|
36
|
+
textarea {
|
|
37
|
+
font-family: inherit;
|
|
38
|
+
font-size: 100%;
|
|
39
|
+
font-weight: inherit;
|
|
40
|
+
line-height: inherit;
|
|
41
|
+
color: inherit;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
button {
|
|
45
|
+
cursor: pointer;
|
|
46
|
+
background: transparent;
|
|
47
|
+
border: none;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
a {
|
|
51
|
+
color: var(--color-text-link);
|
|
52
|
+
text-decoration: underline;
|
|
53
|
+
|
|
54
|
+
&:hover {
|
|
55
|
+
color: var(--color-text-link-hover);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
:focus-visible {
|
|
60
|
+
outline: var(--border-width-medium) solid var(--color-border-focus);
|
|
61
|
+
outline-offset: 2px;
|
|
62
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Sign multiplier for horizontal translate animations: +1 in LTR, -1 in RTL, so
|
|
2
|
+
// a single `translateX(calc(<x> * var(--ea-rtl-sign)))` mirrors automatically.
|
|
3
|
+
:where(:root) {
|
|
4
|
+
--ea-rtl-sign: 1;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
[dir='rtl'] {
|
|
8
|
+
--ea-rtl-sign: -1;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Horizontally mirrors directional glyphs (prev/next chevrons, breadcrumb
|
|
12
|
+
// separators, transfer arrows) when an ancestor sets dir="rtl". Opt in by
|
|
13
|
+
// adding the `ea-rtl-flip` class to the icon element.
|
|
14
|
+
[dir='rtl'] .ea-rtl-flip {
|
|
15
|
+
transform: scaleX(-1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// background-position has no logical keyword, so flip the paginator select's
|
|
19
|
+
// native chevron from the inline-end to the inline-start edge under RTL.
|
|
20
|
+
[dir='rtl'] .ea-paginator__select {
|
|
21
|
+
background-position: left 6px center;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// The tree disclosure chevron points toward the inline-start (left) when
|
|
25
|
+
// collapsed in RTL; the open state still rotates down. A 180deg turn keeps it in
|
|
26
|
+
// pure-rotation space so the expand transition stays smooth. Must live here, not
|
|
27
|
+
// the component, because the dir ancestor is outside its encapsulation scope.
|
|
28
|
+
[dir='rtl'] .ea-tree-node__chevron:not(.ea-tree-node__chevron--open) {
|
|
29
|
+
transform: rotate(180deg);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Native fields use dir="auto" so typed LTR text stays LTR, but that resolves
|
|
33
|
+
// an empty field to LTR and parks its placeholder on the left. Pin them to the
|
|
34
|
+
// inline-end edge so every RTL field reads from the right like the others.
|
|
35
|
+
[dir='rtl'] input[dir='auto'],
|
|
36
|
+
[dir='rtl'] textarea[dir='auto'] {
|
|
37
|
+
text-align: right;
|
|
38
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
@use 'mixins' as ea;
|
|
2
|
+
|
|
3
|
+
// Guard against "sticky hover" on touch devices, which fire `mouseenter` on tap
|
|
4
|
+
// but never the matching `mouseleave`. The directive gates pointer listeners on
|
|
5
|
+
// `(hover: hover)`, but if a tooltip ever slips through keep it invisible on touch.
|
|
6
|
+
.ea-tooltip {
|
|
7
|
+
z-index: var(--z-index-tooltip);
|
|
8
|
+
position: absolute;
|
|
9
|
+
padding: var(--space-1-5) var(--space-2-5);
|
|
10
|
+
font-family: var(--font-family-sans);
|
|
11
|
+
font-size: var(--font-size-xs);
|
|
12
|
+
font-weight: var(--font-weight-medium);
|
|
13
|
+
line-height: var(--line-height-normal);
|
|
14
|
+
white-space: nowrap;
|
|
15
|
+
// When a max-width switches the bubble to multi-line, break long unbreakable
|
|
16
|
+
// strings (URLs, tokens) instead of letting them overflow the bubble.
|
|
17
|
+
overflow-wrap: anywhere;
|
|
18
|
+
border: var(--border-width-thin) solid var(--color-tooltip-border);
|
|
19
|
+
border-radius: var(--radius-md);
|
|
20
|
+
background-color: var(--color-tooltip-surface);
|
|
21
|
+
color: var(--color-neutral-0);
|
|
22
|
+
pointer-events: none;
|
|
23
|
+
animation: ea-tooltip-fade-in var(--duration-fast) var(--ease-out);
|
|
24
|
+
@include ea.elevated-surface-border;
|
|
25
|
+
|
|
26
|
+
@media (hover: none) {
|
|
27
|
+
display: none;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@keyframes ea-tooltip-fade-in {
|
|
32
|
+
from {
|
|
33
|
+
opacity: 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
to {
|
|
37
|
+
opacity: 1;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// Every token-declaring selector is wrapped in :where() so it contributes zero
|
|
2
|
+
// specificity. A consumer's own root-level token override (specificity 0,1,0)
|
|
3
|
+
// then wins in light mode, OS-dark, and forced dark alike, independent of
|
|
4
|
+
// stylesheet load order. The library's own light-to-dark switching still
|
|
5
|
+
// resolves correctly by source order, since the dark blocks come after the
|
|
6
|
+
// light ones at equal (zero) specificity.
|
|
7
|
+
:where(:root) {
|
|
8
|
+
--color-primary-50: #ecf3f9;
|
|
9
|
+
--color-primary-100: #d1e3f0;
|
|
10
|
+
--color-primary-200: #abcbe3;
|
|
11
|
+
--color-primary-300: #7dafd4;
|
|
12
|
+
--color-primary-400: #4b91c3;
|
|
13
|
+
--color-primary-500: #3674a1;
|
|
14
|
+
--color-primary-600: #2a5b7e;
|
|
15
|
+
--color-primary-700: #204560;
|
|
16
|
+
--color-primary-800: #162f41;
|
|
17
|
+
--color-primary-900: #0d1c26;
|
|
18
|
+
|
|
19
|
+
--color-secondary-50: #f2eff5;
|
|
20
|
+
--color-secondary-100: #dfd9e8;
|
|
21
|
+
--color-secondary-200: #c4b9d5;
|
|
22
|
+
--color-secondary-300: #a493be;
|
|
23
|
+
--color-secondary-400: #8169a5;
|
|
24
|
+
--color-secondary-500: #665086;
|
|
25
|
+
--color-secondary-600: #503f69;
|
|
26
|
+
--color-secondary-700: #3d3050;
|
|
27
|
+
--color-secondary-800: #292136;
|
|
28
|
+
--color-secondary-900: #181320;
|
|
29
|
+
|
|
30
|
+
--color-neutral-0: #ffffff;
|
|
31
|
+
--color-neutral-50: #f9fafb;
|
|
32
|
+
--color-neutral-100: #f3f4f6;
|
|
33
|
+
--color-neutral-200: #e5e7eb;
|
|
34
|
+
--color-neutral-300: #d1d5db;
|
|
35
|
+
--color-neutral-400: #9ca3af;
|
|
36
|
+
--color-neutral-500: #6b7280;
|
|
37
|
+
--color-neutral-600: #4b5563;
|
|
38
|
+
--color-neutral-700: #374151;
|
|
39
|
+
--color-neutral-800: #1f2937;
|
|
40
|
+
--color-neutral-900: #111827;
|
|
41
|
+
--color-neutral-950: #030712;
|
|
42
|
+
|
|
43
|
+
--color-success-50: #f0fdf4;
|
|
44
|
+
--color-success-100: #dcfce7;
|
|
45
|
+
--color-success-200: #bbf7d0;
|
|
46
|
+
--color-success-500: #22c55e;
|
|
47
|
+
--color-success-600: #16a34a;
|
|
48
|
+
--color-success-700: #15803d;
|
|
49
|
+
|
|
50
|
+
--color-warning-50: #fffbeb;
|
|
51
|
+
--color-warning-100: #fef3c7;
|
|
52
|
+
--color-warning-200: #fde68a;
|
|
53
|
+
--color-warning-500: #f59e0b;
|
|
54
|
+
--color-warning-600: #d97706;
|
|
55
|
+
--color-warning-700: #b45309;
|
|
56
|
+
|
|
57
|
+
--color-error-50: #fef2f2;
|
|
58
|
+
--color-error-100: #fee2e2;
|
|
59
|
+
--color-error-200: #fecaca;
|
|
60
|
+
--color-error-500: #ef4444;
|
|
61
|
+
--color-error-600: #dc2626;
|
|
62
|
+
--color-error-700: #b91c1c;
|
|
63
|
+
|
|
64
|
+
--color-info-50: #ecfeff;
|
|
65
|
+
--color-info-100: #cffafe;
|
|
66
|
+
--color-info-200: #a5f3fc;
|
|
67
|
+
--color-info-500: #06b6d4;
|
|
68
|
+
--color-info-600: #0891b2;
|
|
69
|
+
--color-info-700: #0e7490;
|
|
70
|
+
|
|
71
|
+
--color-text-primary: var(--color-neutral-900);
|
|
72
|
+
--color-text-secondary: var(--color-neutral-600);
|
|
73
|
+
--color-text-tertiary: var(--color-neutral-400);
|
|
74
|
+
--color-text-disabled: var(--color-neutral-400);
|
|
75
|
+
--color-text-inverse: var(--color-neutral-0);
|
|
76
|
+
--color-text-link: var(--color-primary-600);
|
|
77
|
+
--color-text-link-hover: var(--color-primary-800);
|
|
78
|
+
|
|
79
|
+
// Two-tier surfaces: bg-canvas is the page, bg-base is component surfaces on
|
|
80
|
+
// it (inputs, cards, accordion, popovers). In dark mode bg-base lifts above
|
|
81
|
+
// bg-canvas so surfaces don't vanish into the page.
|
|
82
|
+
--color-bg-canvas: var(--color-neutral-0);
|
|
83
|
+
--color-bg-base: var(--color-neutral-0);
|
|
84
|
+
--color-bg-subtle: var(--color-neutral-50);
|
|
85
|
+
--color-bg-stripe: var(--color-neutral-50);
|
|
86
|
+
// Zebra-stripe fill for table rows: a mid-tone between the base row and the
|
|
87
|
+
// header tint so striped rows read as lighter than the header, not identical to
|
|
88
|
+
// it. Dark mode overrides the ratio to stay equally subtle.
|
|
89
|
+
--color-bg-stripe-subtle: color-mix(
|
|
90
|
+
in srgb,
|
|
91
|
+
var(--color-bg-base) 30%,
|
|
92
|
+
var(--color-bg-stripe)
|
|
93
|
+
);
|
|
94
|
+
--color-bg-muted: var(--color-neutral-100);
|
|
95
|
+
// Soft neutral fill for placeholder surfaces (e.g. avatar initials) when no
|
|
96
|
+
// image is set; light enough to sit gently on a white page
|
|
97
|
+
--color-bg-emphasis: var(--color-neutral-100);
|
|
98
|
+
--color-bg-elevated: var(--color-neutral-0);
|
|
99
|
+
--color-bg-overlay: rgba(0, 0, 0, 0.5);
|
|
100
|
+
|
|
101
|
+
// Tooltips float over arbitrary content, so their surface is a unique tone that
|
|
102
|
+
// never matches a page or palette colour in either theme; the translucent
|
|
103
|
+
// hairline border separates it from same-tone backgrounds.
|
|
104
|
+
--color-tooltip-surface: #1a1b21;
|
|
105
|
+
--color-tooltip-border: rgba(255, 255, 255, 0.15);
|
|
106
|
+
|
|
107
|
+
// Interactive lift layers for hover and active/selected fills. Light surfaces
|
|
108
|
+
// are all near-white and never collapse onto these shades, so solid muted
|
|
109
|
+
// tones read cleanly. Dark mode swaps them for translucent washes (see the
|
|
110
|
+
// dark overrides) because its surfaces all collapse onto the same neutrals,
|
|
111
|
+
// where a solid fill would vanish into whatever it sits on.
|
|
112
|
+
--color-state-hover: var(--color-neutral-100);
|
|
113
|
+
--color-state-active: var(--color-neutral-200);
|
|
114
|
+
|
|
115
|
+
--color-border-subtle: var(--color-neutral-200);
|
|
116
|
+
--color-border-default: var(--color-neutral-200);
|
|
117
|
+
--color-border-strong: var(--color-neutral-400);
|
|
118
|
+
--color-divider: var(--color-border-subtle);
|
|
119
|
+
--color-border-focus: var(--color-primary-500);
|
|
120
|
+
|
|
121
|
+
// brand-default/-hover/-active are the brand colour as a surface (solid bg
|
|
122
|
+
// under white text); needs 4.5:1 vs white. brand-text is the brand colour as
|
|
123
|
+
// a foreground on a non-brand surface; needs 4.5:1 vs bg-base and flips
|
|
124
|
+
// lighter in dark mode.
|
|
125
|
+
--color-brand-default: var(--color-primary-600);
|
|
126
|
+
--color-brand-hover: var(--color-primary-700);
|
|
127
|
+
--color-brand-active: var(--color-primary-800);
|
|
128
|
+
--color-brand-text: var(--color-primary-700);
|
|
129
|
+
--color-brand-subtle: var(--color-primary-50);
|
|
130
|
+
--color-brand-muted: var(--color-primary-100);
|
|
131
|
+
|
|
132
|
+
--color-brand-secondary-default: var(--color-secondary-500);
|
|
133
|
+
--color-brand-secondary-hover: var(--color-secondary-600);
|
|
134
|
+
--color-brand-secondary-active: var(--color-secondary-700);
|
|
135
|
+
--color-brand-secondary-subtle: var(--color-secondary-50);
|
|
136
|
+
--color-brand-secondary-muted: var(--color-secondary-100);
|
|
137
|
+
|
|
138
|
+
// *-text is the status hue as a foreground on its own *-subtle/-muted wash
|
|
139
|
+
// (badge, tag, toast). It needs 4.5:1 vs that wash and flips lighter in dark
|
|
140
|
+
// mode, mirroring --color-brand-text.
|
|
141
|
+
--color-success-default: var(--color-success-600);
|
|
142
|
+
--color-success-subtle: var(--color-success-50);
|
|
143
|
+
--color-success-muted: var(--color-success-100);
|
|
144
|
+
--color-success-text: var(--color-success-700);
|
|
145
|
+
|
|
146
|
+
--color-warning-default: var(--color-warning-600);
|
|
147
|
+
--color-warning-subtle: var(--color-warning-50);
|
|
148
|
+
--color-warning-muted: var(--color-warning-100);
|
|
149
|
+
--color-warning-text: var(--color-warning-700);
|
|
150
|
+
|
|
151
|
+
--color-error-default: var(--color-error-600);
|
|
152
|
+
--color-error-subtle: var(--color-error-50);
|
|
153
|
+
--color-error-muted: var(--color-error-100);
|
|
154
|
+
--color-error-text: var(--color-error-700);
|
|
155
|
+
|
|
156
|
+
--color-info-default: var(--color-info-600);
|
|
157
|
+
--color-info-subtle: var(--color-info-50);
|
|
158
|
+
--color-info-muted: var(--color-info-100);
|
|
159
|
+
--color-info-text: var(--color-info-700);
|
|
160
|
+
|
|
161
|
+
// Pure RGB primaries for the picker's hue wheel and saturation/value
|
|
162
|
+
// gradient. Intrinsic to the picker, not themeable; kept here so component
|
|
163
|
+
// SCSS stays literal-free.
|
|
164
|
+
--color-picker-hue-red: #ff0000;
|
|
165
|
+
--color-picker-hue-yellow: #ffff00;
|
|
166
|
+
--color-picker-hue-green: #00ff00;
|
|
167
|
+
--color-picker-hue-cyan: #00ffff;
|
|
168
|
+
--color-picker-hue-blue: #0000ff;
|
|
169
|
+
--color-picker-hue-magenta: #ff00ff;
|
|
170
|
+
--color-picker-sv-white: #ffffff;
|
|
171
|
+
--color-picker-sv-black: #000000;
|
|
172
|
+
--color-picker-thumb-halo: rgba(0, 0, 0, 0.25);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Dark mode: applied when the OS prefers dark, unless forced light via
|
|
176
|
+
// `<html data-theme="light">`. `<html data-theme="dark">` forces dark.
|
|
177
|
+
@mixin dark-color-tokens {
|
|
178
|
+
--color-text-primary: var(--color-neutral-50);
|
|
179
|
+
--color-text-secondary: var(--color-neutral-300);
|
|
180
|
+
--color-text-tertiary: var(--color-neutral-500);
|
|
181
|
+
--color-text-disabled: var(--color-neutral-500);
|
|
182
|
+
--color-text-inverse: var(--color-neutral-900);
|
|
183
|
+
--color-text-link: var(--color-primary-300);
|
|
184
|
+
--color-text-link-hover: var(--color-primary-100);
|
|
185
|
+
|
|
186
|
+
// Canvas is the deepest page tone; bg-base and up lift component surfaces,
|
|
187
|
+
// striped rows, floating surfaces, and hover states above it.
|
|
188
|
+
--color-bg-canvas: var(--color-neutral-950);
|
|
189
|
+
--color-bg-base: var(--color-neutral-800);
|
|
190
|
+
--color-bg-subtle: var(--color-neutral-700);
|
|
191
|
+
--color-bg-stripe: var(--color-neutral-900);
|
|
192
|
+
// The dark base-to-stripe gap reads stronger than the light one, so weight the
|
|
193
|
+
// stripe mix toward the base row to keep zebra striping subtle.
|
|
194
|
+
--color-bg-stripe-subtle: color-mix(
|
|
195
|
+
in srgb,
|
|
196
|
+
var(--color-bg-base) 62.5%,
|
|
197
|
+
var(--color-bg-stripe)
|
|
198
|
+
);
|
|
199
|
+
--color-bg-elevated: var(--color-neutral-700);
|
|
200
|
+
// Opaque muted surface for static fills (disabled fields, slider/progress
|
|
201
|
+
// tracks, skeletons). Hover/active fills route through the translucent
|
|
202
|
+
// --color-state-* tokens instead, so they never collide with this shade.
|
|
203
|
+
--color-bg-muted: var(--color-neutral-700);
|
|
204
|
+
--color-bg-emphasis: var(--color-neutral-600);
|
|
205
|
+
|
|
206
|
+
// White wash so an interactive lift reads on any dark surface, including the
|
|
207
|
+
// neutral-700 tier where bg-muted/-subtle/-elevated all coincide. This is the
|
|
208
|
+
// fix that lets dropdown/menu/dialog hovers lift off their elevated surfaces.
|
|
209
|
+
--color-state-hover: rgba(255, 255, 255, 0.08);
|
|
210
|
+
--color-state-active: rgba(255, 255, 255, 0.14);
|
|
211
|
+
|
|
212
|
+
// Borders stay clear of every bg-* shade so they stay visible. subtle mixes
|
|
213
|
+
// neutral-700/-800 to sit between card and cell backgrounds; default can't go
|
|
214
|
+
// darker without colliding with bg-subtle/-elevated (both neutral-700).
|
|
215
|
+
--color-border-subtle: color-mix(
|
|
216
|
+
in srgb,
|
|
217
|
+
var(--color-neutral-700),
|
|
218
|
+
var(--color-neutral-800)
|
|
219
|
+
);
|
|
220
|
+
--color-border-default: var(--color-neutral-400);
|
|
221
|
+
--color-border-strong: var(--color-neutral-300);
|
|
222
|
+
// Dividers read as barely-there against dark surfaces at border-subtle, so
|
|
223
|
+
// they step one neutral lighter for a visible-but-quiet rule.
|
|
224
|
+
--color-divider: var(--color-neutral-600);
|
|
225
|
+
|
|
226
|
+
// Surface roles step one shade lighter than light mode so the button clears
|
|
227
|
+
// WCAG 1.4.11 (3:1) on the near-black canvas while keeping a white label
|
|
228
|
+
// above 4.5:1.
|
|
229
|
+
--color-brand-default: var(--color-primary-500);
|
|
230
|
+
--color-brand-hover: var(--color-primary-600);
|
|
231
|
+
--color-brand-active: var(--color-primary-700);
|
|
232
|
+
--color-brand-text: var(--color-primary-300);
|
|
233
|
+
--color-brand-subtle: rgba(75, 145, 195, 0.1);
|
|
234
|
+
--color-brand-muted: rgba(75, 145, 195, 0.2);
|
|
235
|
+
|
|
236
|
+
// Light pastels become unreadable behind light text in dark mode. Re-tint as
|
|
237
|
+
// a low-alpha wash of the saturated *-500 hue so the surface darkens enough
|
|
238
|
+
// for white text.
|
|
239
|
+
--color-success-subtle: rgba(34, 197, 94, 0.15);
|
|
240
|
+
--color-success-muted: rgba(34, 197, 94, 0.25);
|
|
241
|
+
--color-warning-subtle: rgba(245, 158, 11, 0.15);
|
|
242
|
+
--color-warning-muted: rgba(245, 158, 11, 0.25);
|
|
243
|
+
--color-error-subtle: rgba(239, 68, 68, 0.15);
|
|
244
|
+
--color-error-muted: rgba(239, 68, 68, 0.25);
|
|
245
|
+
--color-info-subtle: rgba(6, 182, 212, 0.15);
|
|
246
|
+
--color-info-muted: rgba(6, 182, 212, 0.25);
|
|
247
|
+
|
|
248
|
+
// The *-700 shades are unreadable on the dark translucent washes above; flip
|
|
249
|
+
// to the light *-200 pastels so status text keeps 4.5:1.
|
|
250
|
+
--color-success-text: var(--color-success-200);
|
|
251
|
+
--color-warning-text: var(--color-warning-200);
|
|
252
|
+
--color-error-text: var(--color-error-200);
|
|
253
|
+
--color-info-text: var(--color-info-200);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
@media (prefers-color-scheme: dark) {
|
|
257
|
+
:where(:root:not([data-theme='light'])) {
|
|
258
|
+
@include dark-color-tokens;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
:where(:root[data-theme='dark']) {
|
|
263
|
+
@include dark-color-tokens;
|
|
264
|
+
color-scheme: dark;
|
|
265
|
+
}
|