@zambon-dev/ui 1.0.0 → 1.1.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.
@@ -1,60 +1,86 @@
1
- :root {
2
- /* Dimensions */
3
- --sidebar-animation-duration: 600ms;
4
- --sidebar-collapsed-width: 70px;
5
- --sidebar-expanded-width: 220px;
6
- --sidebar-icon-collapsed-margin: 10px;
7
- --sidebar-item-height: 2.75rem;
8
- --sidebar-logo-height: 50px;
9
- --sidebar-picture-size: 50px;
1
+ /*
2
+ * Component tokens -- the published contract.
3
+ *
4
+ * These names are API: applications override them, and the CHANGELOG documents them. So they stay.
5
+ * What changed is where their values come from. Each one now names a role in the semantic core
6
+ * (see shared/styles/theme.css) instead of repeating a literal, which is what lets a theme move
7
+ * them all at once. The component token is the contract; the core is the implementation.
8
+ *
9
+ * Wrapped in a cascade layer on purpose. Until now an application's override only won if its
10
+ * stylesheet was imported after this one -- same selector, same specificity, later wins. An
11
+ * unlayered declaration beats any layer whatever the order, so an override now wins either way.
12
+ * This can only make an override win more, never less. Override without a layer.
13
+ *
14
+ * A consequence worth knowing: a static override pins that token in both themes. To vary by theme,
15
+ * write `:root[data-theme='dark'] { --sidebar-bg: ... }` -- same specificity as ours, declared
16
+ * later, so it wins. Note that the dark block in theme.css touches core tokens only and never
17
+ * these, so a brand override stays yours in both themes without any work.
18
+ */
19
+ @layer tokens {
20
+ :root {
21
+ /* Dimensions. Geometry of a component rather than design decisions, so they stay literal. */
22
+ --sidebar-collapsed-width: 70px;
23
+ --sidebar-expanded-width: 220px;
24
+ --sidebar-icon-collapsed-margin: 10px;
25
+ --sidebar-item-height: 2.75rem;
26
+ --sidebar-logo-height: 50px;
27
+ --sidebar-picture-size: 50px;
10
28
 
11
- /* Surfaces & colors — translucent "glass" (brand-tinted) */
12
- --sidebar-bg: color-mix(in srgb, var(--color-primary-600) 82%, transparent);
13
- --sidebar-nav-bg: color-mix(in srgb, white 5%, transparent);
14
- --sidebar-text: var(--color-gray-100);
15
- --sidebar-text-muted: color-mix(in srgb, var(--color-gray-100) 60%, transparent);
16
- --sidebar-item-hover-bg: color-mix(in srgb, white 12%, transparent);
17
- --sidebar-item-selected-bg: color-mix(in srgb, white 22%, transparent);
18
- --sidebar-item-selected-text: var(--color-white);
19
- --sidebar-accent: var(--color-gray-100);
20
- --sidebar-tree-line: color-mix(in srgb, white 80%, var(--color-primary-600));
29
+ /* Surfaces & colors -- translucent "glass" (brand-tinted) */
30
+ --sidebar-animation-duration: var(--duration-long);
31
+ --sidebar-bg: var(--color-surface-brand);
32
+ /* A single 5% rung with no pair in the core. Adding one for one use would be a worse trade,
33
+ and it reads correctly in both themes because the surface beneath it is dark in both. */
34
+ --sidebar-nav-bg: color-mix(in srgb, white 5%, transparent);
35
+ --sidebar-text: var(--color-content-on-brand);
36
+ --sidebar-text-muted: var(--color-content-on-brand-muted);
37
+ --sidebar-item-hover-bg: var(--state-hover-on-brand);
38
+ --sidebar-item-selected-bg: var(--state-selected-on-brand);
39
+ --sidebar-item-selected-text: var(--color-content-on-accent);
40
+ --sidebar-accent: var(--color-content-on-brand);
41
+ --sidebar-tree-line: var(--color-line-on-brand);
21
42
 
22
- /* Glass */
23
- --sidebar-blur: 16px;
24
- --sidebar-shadow: 0 10px 40px -12px rgba(0, 0, 0, 0.45);
25
- /* App background the glass sidebar floats over (tune to taste). */
26
- --app-backdrop: linear-gradient(160deg, var(--color-gray-100) 0%, var(--color-gray-300) 100%);
43
+ /* Glass */
44
+ --sidebar-blur: var(--blur-glass);
45
+ --sidebar-shadow: var(--shadow-float-strong);
46
+ /* App background the glass sidebar floats over. A gradient, so it has no home in --color-*;
47
+ the dark theme restates it in theme.css. */
48
+ --app-backdrop: linear-gradient(160deg, var(--color-gray-100) 0%, var(--color-gray-300) 100%);
27
49
 
28
- /* Radii (rounded restyle) */
29
- --sidebar-radius: 1.25rem;
30
- --sidebar-item-radius: 10px;
50
+ /* Radii (rounded restyle) */
51
+ --sidebar-radius: var(--radius-surface);
52
+ --sidebar-item-radius: var(--radius-control);
31
53
 
32
- /* Tabs — a light glass card the views live in. The sidebar's glass is brand-tinted
33
- and dark; this one is white-tinted, because the panel holds forms and grids that
34
- need dark-on-light contrast. Same language (blur, radius, shadow), different hue. */
35
- --tabs-item-bg: color-mix(in srgb, white 45%, transparent);
36
- --tabs-item-hover-bg: color-mix(in srgb, white 70%, transparent);
37
- --tabs-item-active-bg: color-mix(in srgb, white 88%, transparent);
38
- --tabs-item-text: var(--color-gray-600);
39
- --tabs-item-active-text: var(--color-gray-900);
40
- --tabs-content-bg: color-mix(in srgb, white 88%, transparent);
41
- --tabs-border: color-mix(in srgb, white 60%, transparent);
42
- --tabs-close: var(--color-gray-400);
43
- --tabs-close-hover-bg: color-mix(in srgb, var(--color-gray-900) 10%, transparent);
44
- --tabs-close-hover: var(--color-gray-700);
54
+ /* Tabs -- a light glass card the views live in. The sidebar's glass is brand-tinted
55
+ and dark; this one is white-tinted, because the panel holds forms and grids that
56
+ need dark-on-light contrast. Same language (blur, radius, shadow), different hue.
45
57
 
46
- /* Glass — softer than the sidebar's, which floats over the backdrop rather than content. */
47
- --tabs-blur: 16px;
48
- --tabs-shadow: 0 10px 40px -12px rgba(0, 0, 0, 0.25);
58
+ That the active tab and the panel now resolve to one token is the point: they are a single
59
+ surface, which is why no seam shows between them. */
60
+ --tabs-item-bg: var(--color-surface-glass-weak);
61
+ --tabs-item-hover-bg: var(--color-surface-glass);
62
+ --tabs-item-active-bg: var(--color-surface-glass-strong);
63
+ --tabs-item-text: var(--color-content-secondary);
64
+ --tabs-item-active-text: var(--color-content-primary);
65
+ --tabs-content-bg: var(--color-surface-glass-strong);
66
+ --tabs-border: var(--color-line-on-glass);
67
+ --tabs-close: var(--color-content-subtle);
68
+ --tabs-close-hover-bg: var(--state-pressed);
69
+ --tabs-close-hover: var(--color-gray-700);
49
70
 
50
- /* Radii & dimensions */
51
- --tabs-radius: 1.25rem;
52
- --tabs-item-radius: 10px;
53
- --tabs-height: 2.75rem;
54
- --tabs-item-max-width: 14rem;
55
- /* Tabs compress to this before the strip starts scrolling -- still wide enough for a few
56
- characters plus the close button, so a compressed tab is identifiable rather than a stub. */
57
- --tabs-item-min-width: 7rem;
58
- --tabs-scroll-width: 1.75rem;
59
- --tabs-fade-width: 1.5rem;
71
+ /* Glass -- softer than the sidebar's, which floats over the backdrop rather than content. */
72
+ --tabs-blur: var(--blur-glass);
73
+ --tabs-shadow: var(--shadow-float);
74
+
75
+ /* Radii & dimensions */
76
+ --tabs-radius: var(--radius-surface);
77
+ --tabs-item-radius: var(--radius-control);
78
+ --tabs-height: 2.75rem;
79
+ --tabs-item-max-width: 14rem;
80
+ /* Tabs compress to this before the strip starts scrolling -- still wide enough for a few
81
+ characters plus the close button, so a compressed tab is identifiable rather than a stub. */
82
+ --tabs-item-min-width: 7rem;
83
+ --tabs-scroll-width: 1.75rem;
84
+ --tabs-fade-width: 1.5rem;
85
+ }
60
86
  }
@@ -1,6 +1,6 @@
1
1
  import * as i0 from "@angular/core";
2
2
  import { AfterViewInit, ApplicationRef, EnvironmentProviders, EventEmitter, InjectionToken, InputSignal, ModelSignal, NgZone, OnDestroy, OnInit, OutputEmitterRef, Provider, Signal, TemplateRef, WritableSignal } from "@angular/core";
3
- import { AngularAssetPattern, DataGridConfigsProvider, DataGridDataset, DataProviderService, DateValidators, FormService, IModal, IRibbonButtonOption, ITabView, ModalComponent, ModalSize, MultiEditorComponent, MultiEditorDataset, MultiSelectComponent, MultiSelectResultDataset, RibbonButtonComponent, RibbonGroupChild } from "@zambon-dev/ui/library";
3
+ import { AngularAssetPattern, ButtonIntent, DataGridConfigsProvider, DataGridDataset, DataProviderService, DateValidators, FormService, IModal, IRibbonButtonOption, ITabView, ModalComponent, ModalSize, MultiEditorComponent, MultiEditorDataset, MultiSelectComponent, MultiSelectResultDataset, RibbonButtonComponent, RibbonGroupChild } from "@zambon-dev/ui/library";
4
4
  import { Observable, ReplaySubject, Subject } from "rxjs";
5
5
  import { JwtHelperService } from "@auth0/angular-jwt";
6
6
  import { Location } from "@angular/common";
@@ -261,7 +261,12 @@ export declare class ButtonConfirmComponent extends BaseButton {
261
261
  readonly color: InputSignal<string>;
262
262
  readonly icon: InputSignal<string>;
263
263
  readonly label: InputSignal<string>;
264
- readonly modalConfirmButtonColor: InputSignal<string>;
264
+ /**
265
+ * What the confirm button is for. Replaces `modalConfirmButtonColor`, which took a Tailwind
266
+ * palette name -- `green-500` -- and so could neither follow a theme nor say what the button
267
+ * meant.
268
+ */
269
+ readonly modalConfirmButtonIntent: InputSignal<ButtonIntent>;
265
270
  readonly modalConfirmButtonLabel: InputSignal<string>;
266
271
  readonly modalMessage: InputSignal<string>;
267
272
  readonly modalMessageIcon: InputSignal<string>;
@@ -293,8 +298,8 @@ export declare class ButtonConfirmComponent extends BaseButton {
293
298
  "required": false;
294
299
  "isSignal": true;
295
300
  };
296
- "modalConfirmButtonColor": {
297
- "alias": "modalConfirmButtonColor";
301
+ "modalConfirmButtonIntent": {
302
+ "alias": "modalConfirmButtonIntent";
298
303
  "required": false;
299
304
  "isSignal": true;
300
305
  };
@@ -1443,6 +1448,8 @@ export interface AppConfigOptions {
1443
1448
  companyName?: string;
1444
1449
  environment?: string;
1445
1450
  logoUrl?: string;
1451
+ loginBackgroundUrl?: string;
1452
+ loginLogoUrl?: string;
1446
1453
  notificationsEnabled?: boolean;
1447
1454
  notificationsUrl?: string;
1448
1455
  version?: string;
@@ -1457,6 +1464,10 @@ export declare class AppConfig {
1457
1464
  environment: string;
1458
1465
  /** Optional brand logo URL shown in the top bar. */
1459
1466
  logoUrl?: string;
1467
+ /** Optional photograph behind the sign-in screen. Without one the app backdrop shows. */
1468
+ loginBackgroundUrl?: string;
1469
+ /** Optional logo on the sign-in card. Falls back to `logoUrl`. */
1470
+ loginLogoUrl?: string;
1460
1471
  /** Whether the top-bar notifications feature (bell + badge) is enabled. */
1461
1472
  notificationsEnabled: boolean;
1462
1473
  /** SignalR hub URL the notifications service connects to when enabled. */
@@ -10,6 +10,87 @@ export declare abstract class BaseComponent implements OnDestroy {
10
10
  static ɵfac: i0.ɵɵFactoryDeclaration<BaseComponent, never>;
11
11
  static ɵcmp: i0.ɵɵComponentDeclaration<BaseComponent, "ng-component", never, {}, {}, never, never, true, never>;
12
12
  }
13
+ /** What the button is for, which is what decides how loud it looks. */
14
+ type ButtonIntent = 'primary' | 'secondary' | 'danger' | 'ghost';
15
+ /**
16
+ * The one button.
17
+ *
18
+ * It replaces the global `.btn` classes, which generated 154 variants by interpolating Tailwind
19
+ * colour names -- `btn blue-500`, `btn red-700` -- in a `@for` loop. Those named a palette entry
20
+ * rather than a purpose, so a screen's buttons were a colour decision taken 154 times, and none of
21
+ * them could follow a theme. An intent says what the button is for and lets the theme decide what
22
+ * that looks like.
23
+ *
24
+ * `lib-ribbon-button` is deliberately not folded in here. It is a different shape -- icon above
25
+ * label, large hit area, an options menu -- and merging the two would produce one component with
26
+ * two personalities. They share the tokens instead.
27
+ *
28
+ * There is no click output: the inner element is a real `<button>`, so a `(click)` bound on
29
+ * `<lib-button>` receives the bubbled event, and a disabled button emits nothing to bubble. For the
30
+ * same reason `type="submit"` works from inside a form with no wiring.
31
+ */
32
+ export declare class ButtonComponent {
33
+ /** What the button is for. Governs fill and text colour. */
34
+ readonly intent: InputSignal<ButtonIntent>;
35
+ /** Native button type. `submit` works because the rendered button is inside the host's form. */
36
+ readonly type: InputSignal<'button' | 'submit' | 'reset'>;
37
+ readonly disabled: InputSignal<boolean>;
38
+ /** Shows a spinner, announces `aria-busy` and blocks activation -- by the keyboard too. */
39
+ readonly loading: InputSignal<boolean>;
40
+ /** Transient confirmation. Blocks activation so the action cannot be fired twice. */
41
+ readonly success: InputSignal<boolean>;
42
+ /** Transient failure. Leaves the button usable so the action can be retried. */
43
+ readonly error: InputSignal<boolean>;
44
+ /** Font Awesome classes for a leading icon, e.g. `fa-solid fa-floppy-disk`. */
45
+ readonly icon: InputSignal<string | undefined>;
46
+ /** Required when the button has an icon and no text. */
47
+ readonly ariaLabel: InputSignal<string | undefined>;
48
+ /** Loading and success both stand for "this action is settled"; error is retryable. */
49
+ protected readonly isBlocked: Signal<boolean>;
50
+ static ɵfac: i0.ɵɵFactoryDeclaration<ButtonComponent, never>;
51
+ static ɵcmp: i0.ɵɵComponentDeclaration<ButtonComponent, "lib-button", never, {
52
+ "intent": {
53
+ "alias": "intent";
54
+ "required": false;
55
+ "isSignal": true;
56
+ };
57
+ "type": {
58
+ "alias": "type";
59
+ "required": false;
60
+ "isSignal": true;
61
+ };
62
+ "disabled": {
63
+ "alias": "disabled";
64
+ "required": false;
65
+ "isSignal": true;
66
+ };
67
+ "loading": {
68
+ "alias": "loading";
69
+ "required": false;
70
+ "isSignal": true;
71
+ };
72
+ "success": {
73
+ "alias": "success";
74
+ "required": false;
75
+ "isSignal": true;
76
+ };
77
+ "error": {
78
+ "alias": "error";
79
+ "required": false;
80
+ "isSignal": true;
81
+ };
82
+ "icon": {
83
+ "alias": "icon";
84
+ "required": false;
85
+ "isSignal": true;
86
+ };
87
+ "ariaLabel": {
88
+ "alias": "ariaLabel";
89
+ "required": false;
90
+ "isSignal": true;
91
+ };
92
+ }, {}, never, ["*"], true, never>;
93
+ }
13
94
  export interface IBatchUpdate<TEntity, TKey> {
14
95
  entitiesToDelete: Array<TKey>;
15
96
  entitiesToInsertOrUpdate: Array<TEntity>;
@@ -1672,4 +1753,5 @@ export declare class DateValidators {
1672
1753
  static lessThanToday(control: AbstractControl): ValidationErrors | null;
1673
1754
  static maxDate(maxDate: Date): ValidatorFn;
1674
1755
  static minDate(minDate: Date): ValidatorFn;
1675
- }
1756
+ }
1757
+ export type { ButtonIntent };
@@ -548,8 +548,11 @@ export declare function createZambonSharedTranslateLoader(httpBackend: HttpBacke
548
548
  export declare function provideZambonSharedTranslateLoader(appResources?: (string | TranslationResource)[]): Provider;
549
549
  export declare function provideZambonSharedTranslateService(config?: ZambonSharedTranslateConfig): EnvironmentProviders;
550
550
  export declare class LoginLayoutComponent {
551
+ private config;
552
+ /** Optional photograph behind the card. Absent leaves the app backdrop showing. */
553
+ protected get backgroundUrl(): string | undefined;
551
554
  static ɵfac: i0.ɵɵFactoryDeclaration<LoginLayoutComponent, never>;
552
- static ɵcmp: i0.ɵɵComponentDeclaration<LoginLayoutComponent, "shared-login-layout", never, {}, {}, never, never, true, never>;
555
+ static ɵcmp: i0.ɵɵComponentDeclaration<LoginLayoutComponent, "shared-login-layout", never, {}, {}, never, ["*"], true, never>;
553
556
  }
554
557
  export declare class MainLayoutComponent implements OnInit, OnDestroy {
555
558
  private readonly logoutModal;
@@ -672,4 +675,68 @@ export declare class UtcDatePipe implements PipeTransform {
672
675
  transform(value?: Date | string): Date | null;
673
676
  static ɵfac: i0.ɵɵFactoryDeclaration<UtcDatePipe, never>;
674
677
  static ɵpipe: i0.ɵɵPipeDeclaration<UtcDatePipe, "utcDate", true>;
678
+ }
679
+ /** What the user chose. `system` follows the operating system and keeps following it. */
680
+ export type ThemePreference = 'light' | 'dark' | 'system';
681
+ /** What the stylesheet is actually asked for. The CSS never sees `system`. */
682
+ export type ResolvedTheme = 'light' | 'dark';
683
+ /** Key the preference is stored under, and the one the pre-paint snippet has to read. */
684
+ export declare const THEME_STORAGE_KEY = "zambon-theme";
685
+ /**
686
+ * Owns the light/dark choice and writes it to `<html>`.
687
+ *
688
+ * The CSS switch is an attribute rather than `prefers-color-scheme` so that both sides keep a
689
+ * choice: an application can leave the attribute unwritten and never have a dark theme, and a user
690
+ * can pick light on a dark machine. Following the operating system is still a supported choice --
691
+ * it is resolved here, in TypeScript, and written out as a concrete `light` or `dark`. That is why
692
+ * `theme.css` holds one dark block instead of two that would have to be kept identical.
693
+ *
694
+ * The attribute is normally written before this service exists, by the snippet in `index.html`
695
+ * documented in the design-tokens guide -- an Angular service cannot run before the first paint,
696
+ * and a frame of the wrong theme is worse than no theme. This picks up from there.
697
+ */
698
+ export declare class ThemeService {
699
+ private readonly document;
700
+ private readonly preferenceState;
701
+ private readonly systemPrefersDark;
702
+ /** What the user chose, including `system`. */
703
+ readonly preference: import("@angular/core").Signal<ThemePreference>;
704
+ /** What is actually applied right now. `system` is already resolved here. */
705
+ readonly resolved: import("@angular/core").Signal<ResolvedTheme>;
706
+ constructor();
707
+ /** Records a choice and applies it. Persisting is best-effort: a blocked store is not an error. */
708
+ setPreference(preference: ThemePreference): void;
709
+ /** Steps light -> dark -> system -> light, which is what a single-button switch needs. */
710
+ cyclePreference(): void;
711
+ private readStoredPreference;
712
+ private readSystemPrefersDark;
713
+ /**
714
+ * Keeps following the operating system after the first read. Without this, choosing `system` and
715
+ * then switching the machine to dark would leave the application on light until a reload.
716
+ */
717
+ private watchSystemPreference;
718
+ static ɵfac: i0.ɵɵFactoryDeclaration<ThemeService, never>;
719
+ static ɵprov: i0.ɵɵInjectableDeclaration<any>;
720
+ }
721
+ /**
722
+ * The top bar's light/dark control.
723
+ *
724
+ * One button stepping light -> dark -> system rather than three, because the bar is tight and this
725
+ * is not a setting people change often. The icon shows the choice that is in force, not the one a
726
+ * click would bring: a control that displays its own next state cannot be read at a glance.
727
+ *
728
+ * It has no stylesheet: a ghost `lib-button` is already the icon-button treatment, and a second
729
+ * copy of it here would drift from the one in the top bar beside it.
730
+ */
731
+ export declare class ThemeSwitchComponent {
732
+ private readonly theme;
733
+ /** The choice in force, including `system`. */
734
+ protected readonly preference: import("@angular/core").Signal<ThemePreference>;
735
+ /** Full Font Awesome classes for the current choice -- the style prefix is not optional. */
736
+ protected readonly icon: import("@angular/core").Signal<string>;
737
+ /** Translation key naming the current choice, used for the label and the tooltip. */
738
+ protected readonly label: import("@angular/core").Signal<string>;
739
+ protected onClick(): void;
740
+ static ɵfac: i0.ɵɵFactoryDeclaration<ThemeSwitchComponent, never>;
741
+ static ɵcmp: i0.ɵɵComponentDeclaration<ThemeSwitchComponent, "shared-theme-switch", never, {}, {}, never, never, true, never>;
675
742
  }
@@ -1,3 +1,6 @@
1
+ import { ButtonComponent as LibButtonComponent } from "@zambon-dev/ui/library";
2
+ import { ButtonComponent } from "@zambon-dev/ui/framework";
1
3
  export * from "@zambon-dev/ui/library";
2
4
  export * from "@zambon-dev/ui/framework";
3
- export * from "@zambon-dev/ui/shared";
5
+ export * from "@zambon-dev/ui/shared";
6
+ export { ButtonComponent, LibButtonComponent };
@@ -1,101 +0,0 @@
1
- input[type='button'],
2
- input[type='reset'],
3
- input[type='submit'],
4
- button,
5
- a {
6
- &.btn {
7
- @apply relative px-3 py-1.5 inline-block text-center rounded-sm select-none;
8
- @apply bg-slate-700 text-white;
9
- @apply transition-all;
10
-
11
- @apply focus:ring-4 focus:ring-slate-600/30;
12
- @apply focus:bg-slate-800 hover:bg-slate-800;
13
- @apply active:bg-slate-900;
14
-
15
- i:not(:last-child) {
16
- @apply mr-2;
17
- }
18
-
19
- &.disabled,
20
- &:disabled {
21
- @apply pointer-events-none opacity-[.60] ring-0;
22
- }
23
-
24
- &.error {
25
- @apply bg-red-700 focus:ring-red-600/30;
26
- @apply focus:bg-red-800 hover:bg-red-800;
27
- @apply active:bg-red-900;
28
-
29
- i:before {
30
- @apply content-['\f00d'];
31
- }
32
- }
33
-
34
- &.success {
35
- @apply bg-green-700 ring-4 ring-green-600/30;
36
- @apply pointer-events-none opacity-[.60];
37
-
38
- i:before {
39
- @apply content-['\f00c'];
40
- }
41
- }
42
-
43
- &.loading {
44
- @apply pointer-events-none opacity-[.60] ring-0 pl-8;
45
- @apply transition-[margin-left] duration-200 ease-in-out;
46
-
47
- &:before {
48
- @apply content-[''] absolute left-2 top-0 bg-center bg-contain bg-no-repeat w-5 h-full animate-spin text-white;
49
- @apply transition-opacity duration-200 ease-in-out;
50
- background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"%3E%3Ccircle style="opacity:.25" cx="12" cy="12" r="10" stroke="white" stroke-width="4"%3E%3C/circle%3E%3Cpath style="opacity:.75" fill="white" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"%3E%3C/path%3E%3C/svg%3E');
51
- }
52
-
53
- i {
54
- @apply invisible;
55
- }
56
- }
57
- }
58
- }
59
-
60
- @mixin button($color) {
61
- input[type='button'],
62
- input[type='reset'],
63
- input[type='submit'],
64
- button,
65
- a {
66
- &.btn {
67
- @for $i from 1 through 7 {
68
- &.#{$color + '-' + $i + '00'} {
69
- @if $i > 4 {
70
- @apply text-white;
71
- } @else {
72
- @apply text-black;
73
-
74
- &.loading:before {
75
- background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"%3E%3Ccircle style="opacity:.25" cx="12" cy="12" r="10" stroke="black" stroke-width="4"%3E%3C/circle%3E%3Cpath style="opacity:.75" fill="black" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"%3E%3C/path%3E%3C/svg%3E');
76
- }
77
- }
78
-
79
- @apply #{'bg-' + $color + '-' + $i + '00'};
80
-
81
- $ring: $i + '00/30';
82
- @if $i < 5 {
83
- $ring: '400/20';
84
- }
85
-
86
- @apply #{'focus:ring-' + $color + '-' + $ring};
87
- @apply #{'focus:bg-' + $color + '-' + ($i+1) + '00'};
88
- @apply #{'hover:bg-' + $color + '-' + ($i+1) + '00'};
89
- @apply #{'active:bg-' + $color + '-' + ($i+2) + '00'};
90
- }
91
- }
92
- }
93
- }
94
- }
95
-
96
- $colors:
97
- 'slate', 'gray', 'zinc', 'neutral', 'stone', 'red', 'orange', 'amber', 'yellow', 'lime', 'green', 'emerald', 'teal',
98
- 'cyan', 'sky', 'indigo', 'violet', 'purple', 'fuchsia', 'pink', 'rose', 'primary';
99
- @each $color in $colors {
100
- @include button($color);
101
- }