@firebase-oss/ui-styles 0.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/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # @firebase-oss/ui-styles
2
+
3
+ This package contains the styles for the FirebaseUI components.
4
+
5
+ ## Installation
6
+
7
+ ### With tailwind
8
+
9
+ If you are using Tailwind CSS in your project, you can import the source files directly into your project.
10
+
11
+ ```css
12
+ @import "tailwindcss";
13
+ @import "@firebase-oss/ui-styles/tailwind";
14
+ ```
15
+
16
+ ### With CSS
17
+
18
+ Alternatively, you can import fully compiled CSS files into your project. This output contains both the tailwind styles and the FirebaseUI styles.
19
+
20
+ ```jsx
21
+ import "@firebase-oss/ui-styles";
22
+ ```
23
+
24
+ ## Themes
25
+
26
+ The packages also exports themes which overrides the CSS variables with preset colors. These can be imported from your CSS:
27
+
28
+ ```css
29
+ @import "tailwindcss";
30
+ @import "@firebase-oss/ui-styles/tailwind";
31
+ @import "@firebase-oss/ui-styles/themes/brualist";
32
+ ```
33
+
34
+ ## Building
35
+
36
+ To build the styles into a single CSS file, run the following command:
37
+
38
+ ```bash
39
+ pnpm build
40
+ ```
41
+
42
+ This command will source the `src.css` file and output the compiled CSS to the `dist.css` file.
package/dist.css ADDED
@@ -0,0 +1,2 @@
1
+ /*! tailwindcss v4.1.16 | MIT License | https://tailwindcss.com */
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}@layer theme{:root,:host{--color-red-500:oklch(63.7% .237 25.331);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-800:oklch(27.8% .033 256.848);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--radius-sm:.25rem;--radius-xl:.75rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--radius:var(--fui-radius);--color-primary:var(--fui-primary);--color-primary-hover:var(--fui-primary-hover);--color-primary-surface:var(--fui-primary-surface);--color-text:var(--fui-text);--color-text-muted:var(--fui-text-muted);--color-background:var(--fui-background);--color-border:var(--fui-border);--color-input:var(--fui-input);--color-error:var(--fui-error);--radius-card:var(--fui-radius-card)}:root{--fui-primary:var(--color-black);--fui-primary-hover:var(--fui-primary)}@supports (color:color-mix(in lab, red, red)){:root{--fui-primary-hover:color-mix(in oklab,var(--fui-primary)85%,transparent)}}:root{--fui-primary-surface:var(--color-white);--fui-text:var(--color-black);--fui-text-muted:var(--color-gray-800);--fui-background:var(--color-white);--fui-border:var(--color-gray-200);--fui-input:var(--color-gray-300);--fui-error:var(--color-red-500);--fui-radius:var(--radius-sm);--fui-radius-card:var(--radius-xl)}@media (prefers-color-scheme:dark){:root{--fui-primary:var(--color-white);--fui-primary-hover:var(--fui-primary)}@supports (color:color-mix(in lab, red, red)){:root{--fui-primary-hover:color-mix(in oklab,var(--fui-primary)85%,transparent)}}:root{--fui-primary-surface:var(--color-black);--fui-text:var(--color-white);--fui-text-muted:var(--color-gray-200);--fui-background:var(--color-black);--fui-border:var(--color-gray-200);--fui-input:var(--color-gray-300);--fui-error:var(--color-red-500);--fui-radius:var(--radius-sm);--fui-radius-card:var(--radius-xl)}}}@layer components{:where(.fui-screen){max-width:var(--container-md);padding-top:calc(var(--spacing)*24);margin-inline:auto}:where(:where(.fui-screen .fui-screen__children)>:not(:last-child)),:where(:where(.fui-content)>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(:where(.fui-card)>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}:where(.fui-card){border-radius:var(--radius-card);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-border);background-color:var(--color-background);padding:calc(var(--spacing)*10)}:where(:where(.fui-card__header)>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.fui-card__header){text-align:center}:where(.fui-card__title){font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height));--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold);color:var(--color-text)}:where(.fui-card__subtitle){font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-text-muted)}:where(:where(.fui-form)>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}:where(.fui-form fieldset),:where(.fui-form fieldset label){gap:calc(var(--spacing)*2);color:var(--color-text);flex-direction:column;display:flex}:where(.fui-form fieldset label div[data-input-label]){gap:calc(var(--spacing)*3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);display:flex}:where(.fui-form fieldset label div[data-input-label]>div:first-child){flex-grow:1}:where(.fui-form .fui-form__action){padding-inline:calc(var(--spacing)*1);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-text-muted)}@media (hover:hover){:where(.fui-form .fui-form__action):hover{text-decoration-line:underline}}:where(.fui-form fieldset label input){border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-input);width:100%;padding-inline:calc(var(--spacing)*2);padding-block:calc(var(--spacing)*2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:#0000}:where(.fui-form fieldset label input):focus{outline-style:var(--tw-outline-style);outline-width:2px;outline-color:var(--color-primary)}:where(.fui-form fieldset label input[aria-invalid=true]){border-color:var(--color-error);outline-style:var(--tw-outline-style);outline-width:2px;outline-color:var(--color-error)}:where(.fui-form fieldset label div[data-input-group]){align-items:center;gap:calc(var(--spacing)*2);display:flex}:where(.fui-form .fui-form__error){text-align:left;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-error)}:where(.fui-success){text-align:center;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}:where(.fui-button){justify-content:center;align-items:center;gap:calc(var(--spacing)*3);border-radius:var(--radius);background-color:var(--color-primary);width:100%;padding-inline:calc(var(--spacing)*4);padding-block:calc(var(--spacing)*2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-primary-surface);--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));display:flex}@media (hover:hover){:where(.fui-button):hover{cursor:pointer;background-color:var(--color-primary-hover)}}:where(.fui-button):disabled{cursor:not-allowed;opacity:.5}:where(.fui-button--secondary){border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-input);color:var(--color-text);background-color:#0000}@media (hover:hover){:where(.fui-button--secondary):hover{border-color:var(--color-primary);background-color:var(--color-background)}}:where(.fui-button.fui-provider__button){border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-border)}:where(.fui-provider__button>svg,.fui-provider__button>img){height:calc(var(--spacing)*5);width:calc(var(--spacing)*5)}:where(.fui-divider){margin-block:calc(var(--spacing)*4);align-items:center;gap:calc(var(--spacing)*3);display:flex}:where(.fui-divider__line){background-color:var(--color-border);flex:1;height:1px}:where(.fui-divider__text){font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-text-muted)}:where(.fui-phone-input){align-items:center;gap:calc(var(--spacing)*2);display:flex}:where(.fui-phone-input__number-input){border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-input);padding-inline:calc(var(--spacing)*2);padding-block:calc(var(--spacing)*2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:#0000;flex:1}:where(.fui-phone-input__number-input):focus{outline-style:var(--tw-outline-style);outline-width:2px;outline-color:var(--color-primary)}:where(.fui-phone-input__number-input[aria-invalid=true]){outline-style:var(--tw-outline-style);outline-width:2px;outline-color:var(--color-error)}:where(.fui-country-selector){width:120px;display:inline-block;position:relative}:where(.fui-country-selector__wrapper){border-radius:var(--radius);outline-style:var(--tw-outline-style);outline-width:1px;outline-color:var(--color-input);background-color:#0000;align-items:center;display:flex;position:relative;overflow:hidden}:where(.fui-country-selector__flag){pointer-events:none;left:calc(var(--spacing)*2);font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));position:absolute}:where(.fui-country-selector select){cursor:pointer;appearance:none;width:100%;padding-block:calc(var(--spacing)*2);padding-right:calc(var(--spacing)*2);padding-left:calc(var(--spacing)*8);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:#0000;--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:#0000}:where(.fui-country-selector select):focus{outline-style:var(--tw-outline-style);outline-width:2px;outline-color:var(--color-primary)}:where(.fui-country-selector__dial-code){pointer-events:none;top:50%;left:calc(var(--spacing)*8);--tw-translate-y:calc(calc(1/2*100%)*-1);translate:var(--tw-translate-x)var(--tw-translate-y);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-text);position:absolute}:where(.fui-form fieldset label div[data-input-group]:has(input[aria-invalid=true]) .fui-country-selector),:where(.fui-form fieldset label div[data-input-group]:has(input[aria-invalid=true]) .fui-country-selector .fui-country-selector__wrapper){border-radius:var(--radius);outline-style:var(--tw-outline-style);outline-width:2px;outline-color:var(--color-error)}:where(.fui-policies){text-align:center;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-text-muted)}:where(.fui-policies a,.fui-policies button){--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}@media (hover:hover){:where(.fui-policies a,.fui-policies button):hover{text-decoration-line:underline}}.fui-provider__button[data-provider=google\.com][data-themed=true]{--google-primary:#131314;--color-primary:var(--google-primary);--color-primary-hover:var(--google-primary)}@supports (color:color-mix(in lab, red, red)){.fui-provider__button[data-provider=google\.com][data-themed=true]{--color-primary-hover:color-mix(in oklab,var(--google-primary)85%,transparent)}}.fui-provider__button[data-provider=google\.com][data-themed=true]{--color-primary-surface:#fff;--color-border:var(--google-primary)}.fui-provider__button[data-provider=google\.com][data-themed=neutral]{--google-primary:#f2f2f2;--color-primary:var(--google-primary);--color-primary-hover:var(--google-primary)}@supports (color:color-mix(in lab, red, red)){.fui-provider__button[data-provider=google\.com][data-themed=neutral]{--color-primary-hover:color-mix(in oklab,var(--google-primary)85%,transparent)}}.fui-provider__button[data-provider=google\.com][data-themed=neutral]{--color-primary-surface:#1f1f1f;--color-border:transparent}@media (prefers-color-scheme:dark){.fui-provider__button[data-provider=google\.com][data-themed=true]{--google-primary:#fff;--color-primary:var(--google-primary);--color-primary-hover:var(--google-primary)}@supports (color:color-mix(in lab, red, red)){.fui-provider__button[data-provider=google\.com][data-themed=true]{--color-primary-hover:color-mix(in oklab,var(--google-primary)85%,transparent)}}.fui-provider__button[data-provider=google\.com][data-themed=true]{--color-primary-surface:#1f1f1f;--color-border:#747775}}.fui-provider__button[data-provider=facebook\.com][data-themed=true]{--facebook-primary:#1877f2;--color-primary:var(--facebook-primary);--color-primary-hover:var(--facebook-primary)}@supports (color:color-mix(in lab, red, red)){.fui-provider__button[data-provider=facebook\.com][data-themed=true]{--color-primary-hover:color-mix(in oklab,var(--facebook-primary)85%,transparent)}}.fui-provider__button[data-provider=facebook\.com][data-themed=true]{--color-primary-surface:var(--color-white);--color-border:var(--facebook-primary)}.fui-provider__button[data-provider=apple\.com][data-themed=true]{--apple-primary:#000;--color-primary:var(--apple-primary);--color-primary-hover:var(--apple-primary)}@supports (color:color-mix(in lab, red, red)){.fui-provider__button[data-provider=apple\.com][data-themed=true]{--color-primary-hover:color-mix(in oklab,var(--apple-primary)85%,transparent)}}.fui-provider__button[data-provider=apple\.com][data-themed=true]{--color-primary-surface:#fff;--color-border:var(--apple-primary)}@media (prefers-color-scheme:dark){.fui-provider__button[data-provider=apple\.com][data-themed=true]{--apple-primary:var(--color-white);--color-primary:var(--apple-primary);--color-primary-hover:var(--apple-primary)}@supports (color:color-mix(in lab, red, red)){.fui-provider__button[data-provider=apple\.com][data-themed=true]{--color-primary-hover:color-mix(in oklab,var(--apple-primary)85%,transparent)}}.fui-provider__button[data-provider=apple\.com][data-themed=true]{--color-primary-surface:var(--color-black);--color-border:var(--color-white)}}.fui-provider__button[data-provider=github\.com][data-themed=true]{--github-primary:#000;--color-primary:var(--github-primary);--color-primary-hover:var(--github-primary)}@supports (color:color-mix(in lab, red, red)){.fui-provider__button[data-provider=github\.com][data-themed=true]{--color-primary-hover:color-mix(in oklab,var(--github-primary)85%,transparent)}}.fui-provider__button[data-provider=github\.com][data-themed=true]{--color-primary-surface:#fff;--color-border:var(--github-primary)}@media (prefers-color-scheme:dark){.fui-provider__button[data-provider=github\.com][data-themed=true]{--github-primary:var(--color-white);--color-primary:var(--github-primary);--color-primary-hover:var(--github-primary)}@supports (color:color-mix(in lab, red, red)){.fui-provider__button[data-provider=github\.com][data-themed=true]{--color-primary-hover:color-mix(in oklab,var(--github-primary)85%,transparent)}}.fui-provider__button[data-provider=github\.com][data-themed=true]{--color-primary-surface:var(--color-black);--color-border:var(--color-white)}}.fui-provider__button[data-provider=microsoft\.com][data-themed=true]{--microsoft-primary:#2f2f2f;--color-primary:var(--microsoft-primary);--color-primary-hover:var(--microsoft-primary)}@supports (color:color-mix(in lab, red, red)){.fui-provider__button[data-provider=microsoft\.com][data-themed=true]{--color-primary-hover:color-mix(in oklab,var(--microsoft-primary)85%,transparent)}}.fui-provider__button[data-provider=microsoft\.com][data-themed=true]{--color-primary-surface:#fff;--color-border:var(--microsoft-primary)}@media (prefers-color-scheme:dark){.fui-provider__button[data-provider=microsoft\.com][data-themed=true]{--microsoft-primary:var(--color-white);--color-primary:var(--microsoft-primary);--color-primary-hover:var(--microsoft-primary)}@supports (color:color-mix(in lab, red, red)){.fui-provider__button[data-provider=microsoft\.com][data-themed=true]{--color-primary-hover:color-mix(in oklab,var(--microsoft-primary)85%,transparent)}}.fui-provider__button[data-provider=microsoft\.com][data-themed=true]{--color-primary-surface:#5e5e5e;--color-border:var(--color-white)}}.fui-provider__button[data-provider=twitter\.com][data-themed=true]{--twitter-primary:#1da1f2;--color-primary:var(--twitter-primary);--color-primary-hover:var(--twitter-primary)}@supports (color:color-mix(in lab, red, red)){.fui-provider__button[data-provider=twitter\.com][data-themed=true]{--color-primary-hover:color-mix(in oklab,var(--twitter-primary)85%,transparent)}}.fui-provider__button[data-provider=twitter\.com][data-themed=true]{--color-primary-surface:#fff;--color-border:var(--twitter-primary)}}@layer utilities;@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@firebase-oss/ui-styles",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "style": "./dist.css",
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ },
15
+ "./dist.css": "./dist.css",
16
+ "./tailwind": "./src/base.css",
17
+ "./themes/*": "./src/themes/*.css"
18
+ },
19
+ "sideEffects": [
20
+ "**/*.css"
21
+ ],
22
+ "files": [
23
+ "dist.css",
24
+ "src"
25
+ ],
26
+ "scripts": {
27
+ "prepare": "pnpm run build",
28
+ "build": "tsup && pnpm run build:css",
29
+ "build:css": "pnpm dlx @tailwindcss/cli -i ./src.css -o ./dist.css --minify",
30
+ "build:local": "pnpm run build && pnpm pack",
31
+ "lint": "tsc --noEmit",
32
+ "format": "prettier --write \"src/**/*.ts\"",
33
+ "clean": "rimraf dist",
34
+ "test": "vitest run",
35
+ "test:watch": "vitest",
36
+ "publish:tags": "sh -c 'TAG=\"${npm_package_name}@${npm_package_version}\"; git tag --list \"$TAG\" | grep . || git tag \"$TAG\"; git push origin \"$TAG\"'",
37
+ "release": "pnpm run build && pnpm pack --pack-destination ../../releases/"
38
+ },
39
+ "devDependencies": {
40
+ "rimraf": "catalog:",
41
+ "tailwindcss": "catalog:",
42
+ "tsup": "catalog:",
43
+ "typescript": "catalog:",
44
+ "vitest": "catalog:"
45
+ },
46
+ "dependencies": {
47
+ "cva": "1.0.0-beta.4"
48
+ }
49
+ }
package/src/base.css ADDED
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Copyright 2025 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ @layer theme {
18
+ :root {
19
+ /* The primary color is used for the button and link colors */
20
+ --fui-primary: var(--color-black);
21
+ /* The primary hover color is used for the button and link colors when hovered */
22
+ --fui-primary-hover: --alpha(var(--fui-primary) / 85%);
23
+ /* The primary surface color is used for the button text color */
24
+ --fui-primary-surface: var(--color-white);
25
+ /* The text color used for body text */
26
+ --fui-text: var(--color-black);
27
+ /* The muted text color used for body text, such as subtitles */
28
+ --fui-text-muted: var(--color-gray-800);
29
+ /* The background color of the cards */
30
+ --fui-background: var(--color-white);
31
+ /* The border color used for none input fields */
32
+ --fui-border: var(--color-gray-200);
33
+ /* The input color used for input fields */
34
+ --fui-input: var(--color-gray-300);
35
+ /* The error color used for error messages */
36
+ --fui-error: var(--color-red-500);
37
+ /* The radius used for the input fields */
38
+ --fui-radius: var(--radius-sm);
39
+ /* The radius used for the cards */
40
+ --fui-radius-card: var(--radius-xl);
41
+ }
42
+
43
+ /* Apply dark mode styles when the dark variant is applied */
44
+ /* See https://tailwindcss.com/docs/dark-mode */
45
+ @variant dark {
46
+ :root {
47
+ --fui-primary: var(--color-white);
48
+ --fui-primary-hover: --alpha(var(--fui-primary) / 85%);
49
+ --fui-primary-surface: var(--color-black);
50
+ --fui-text: var(--color-white);
51
+ --fui-text-muted: var(--color-gray-200);
52
+ --fui-background: var(--color-black);
53
+ --fui-border: var(--color-gray-200);
54
+ --fui-input: var(--color-gray-300);
55
+ --fui-error: var(--color-red-500);
56
+ --fui-radius: var(--radius-sm);
57
+ --fui-radius-card: var(--radius-xl);
58
+ }
59
+ }
60
+ }
61
+
62
+ @theme {
63
+ --color-primary: var(--fui-primary);
64
+ --color-primary-hover: var(--fui-primary-hover);
65
+ --color-primary-surface: var(--fui-primary-surface);
66
+ --color-text: var(--fui-text);
67
+ --color-text-muted: var(--fui-text-muted);
68
+ --color-background: var(--fui-background);
69
+ --color-border: var(--fui-border);
70
+ --color-input: var(--fui-input);
71
+ --color-error: var(--fui-error);
72
+ --radius: var(--fui-radius);
73
+ --radius-card: var(--fui-radius-card);
74
+ }
75
+
76
+ @layer components {
77
+ /* Using :where() to reduce CSS specificity - allows users to easily override with their own Tailwind classes */
78
+ :where(.fui-screen) {
79
+ @apply pt-24 max-w-md mx-auto;
80
+ }
81
+
82
+ :where(.fui-screen .fui-screen__children) {
83
+ @apply space-y-2;
84
+ }
85
+
86
+ :where(.fui-content) {
87
+ @apply space-y-2;
88
+
89
+ }
90
+
91
+ :where(.fui-card) {
92
+ @apply bg-background p-10 border border-border rounded-card space-y-6;
93
+ }
94
+
95
+ :where(.fui-card__header) {
96
+ @apply text-center space-y-1;
97
+ }
98
+
99
+ :where(.fui-card__title) {
100
+ @apply text-xl font-bold text-text;
101
+ }
102
+
103
+ :where(.fui-card__subtitle) {
104
+ @apply text-sm text-text-muted;
105
+ }
106
+
107
+ :where(.fui-form) {
108
+ @apply space-y-6;
109
+ }
110
+
111
+ :where(.fui-form fieldset),
112
+ :where(.fui-form fieldset label) {
113
+ @apply flex flex-col gap-2 text-text;
114
+ }
115
+
116
+ :where(.fui-form fieldset label div[data-input-label]) {
117
+ @apply flex gap-3 text-sm font-medium;
118
+ }
119
+
120
+ :where(.fui-form fieldset label div[data-input-label] > div:first-child) {
121
+ @apply grow;
122
+ }
123
+
124
+ :where(.fui-form .fui-form__action) {
125
+ @apply px-1 hover:underline text-xs text-text-muted;
126
+ }
127
+
128
+ :where(.fui-form fieldset label input) {
129
+ @apply w-full border border-input rounded px-2 py-2 text-sm focus:outline-2 focus:outline-primary shadow-xs bg-transparent;
130
+ }
131
+
132
+ :where(.fui-form fieldset label input[aria-invalid="true"]) {
133
+ @apply border-error outline-error outline-2;
134
+ }
135
+
136
+ :where(.fui-form fieldset label div[data-input-group]) {
137
+ @apply flex items-center gap-2;
138
+ }
139
+
140
+ :where(.fui-form .fui-form__error) {
141
+ @apply text-error text-left text-xs;
142
+ }
143
+
144
+ :where(.fui-success) {
145
+ @apply text-center text-xs;
146
+ }
147
+
148
+ :where(.fui-button) {
149
+ @apply w-full flex items-center justify-center gap-3 px-4 py-2 rounded text-sm font-medium shadow-xs bg-primary text-primary-surface transition hover:bg-primary-hover hover:cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed;
150
+ }
151
+
152
+ :where(.fui-button--secondary) {
153
+ @apply bg-transparent text-text border border-input hover:bg-background hover:border-primary;
154
+ }
155
+
156
+ :where(.fui-button.fui-provider__button) {
157
+ @apply border border-border;
158
+ }
159
+
160
+ :where(.fui-provider__button > svg, .fui-provider__button > img) {
161
+ @apply w-5 h-5;
162
+ }
163
+
164
+ :where(.fui-divider) {
165
+ @apply flex items-center gap-3 my-4;
166
+ }
167
+
168
+ :where(.fui-divider__line) {
169
+ @apply flex-1 h-px bg-border;
170
+ }
171
+
172
+ :where(.fui-divider__text) {
173
+ @apply text-text-muted text-xs;
174
+ }
175
+
176
+ :where(.fui-phone-input) {
177
+ @apply flex gap-2 items-center;
178
+ }
179
+
180
+ :where(.fui-phone-input__number-input) {
181
+ @apply border-1 border-input rounded px-2 py-2 text-sm focus:outline-2 focus:outline-primary shadow-xs bg-transparent flex-1;
182
+ }
183
+
184
+ :where(.fui-phone-input__number-input[aria-invalid="true"]) {
185
+ @apply outline-error outline-2;
186
+ }
187
+
188
+ :where(.fui-country-selector) {
189
+ @apply relative inline-block w-[120px];
190
+ }
191
+
192
+ :where(.fui-country-selector__wrapper) {
193
+ @apply relative flex items-center outline-1 outline-input rounded bg-transparent overflow-hidden;
194
+ }
195
+
196
+ :where(.fui-country-selector__flag) {
197
+ @apply absolute left-2 text-lg pointer-events-none;
198
+ }
199
+
200
+ :where(.fui-country-selector select) {
201
+ @apply w-full pl-8 pr-2 py-2 text-sm focus:outline-2 focus:outline-primary shadow-xs bg-transparent appearance-none cursor-pointer text-transparent;
202
+ }
203
+
204
+ :where(.fui-country-selector__dial-code) {
205
+ @apply absolute left-8 top-1/2 -translate-y-1/2 text-sm pointer-events-none text-text;
206
+ }
207
+
208
+ :where(.fui-form fieldset label div[data-input-group]:has(input[aria-invalid="true"]) .fui-country-selector) {
209
+ @apply outline-error outline-2 rounded;
210
+ }
211
+
212
+ :where(.fui-form fieldset label div[data-input-group]:has(input[aria-invalid="true"]) .fui-country-selector .fui-country-selector__wrapper) {
213
+ @apply outline-error outline-2 rounded;
214
+ }
215
+
216
+ :where(.fui-policies) {
217
+ @apply text-text-muted text-center text-xs;
218
+ }
219
+
220
+ :where(.fui-policies a, .fui-policies button) {
221
+ @apply hover:underline font-semibold;
222
+ }
223
+
224
+ .fui-provider__button[data-provider="google.com"][data-themed="true"] {
225
+ --google-primary: #131314;
226
+ --color-primary: var(--google-primary);
227
+ --color-primary-hover: --alpha(var(--google-primary) / 85%);
228
+ --color-primary-surface: #FFFFFF;
229
+ --color-border: var(--google-primary);
230
+ }
231
+
232
+ .fui-provider__button[data-provider="google.com"][data-themed="neutral"] {
233
+ --google-primary: #F2F2F2;
234
+ --color-primary: var(--google-primary);
235
+ --color-primary-hover: --alpha(var(--google-primary) / 85%);
236
+ --color-primary-surface: #1F1F1F;
237
+ --color-border: transparent;
238
+ }
239
+
240
+ @variant dark {
241
+ .fui-provider__button[data-provider="google.com"][data-themed="true"] {
242
+ --google-primary: #FFFFFF;
243
+ --color-primary: var(--google-primary);
244
+ --color-primary-hover: --alpha(var(--google-primary) / 85%);
245
+ --color-primary-surface: #1F1F1F;
246
+ --color-border: #747775;
247
+ }
248
+ }
249
+
250
+ .fui-provider__button[data-provider="facebook.com"][data-themed="true"] {
251
+ --facebook-primary: #1877F2;
252
+ --color-primary: var(--facebook-primary);
253
+ --color-primary-hover: --alpha(var(--facebook-primary) / 85%);
254
+ --color-primary-surface: var(--color-white);
255
+ --color-border: var(--facebook-primary);
256
+ }
257
+
258
+ .fui-provider__button[data-provider="apple.com"][data-themed="true"] {
259
+ --apple-primary: #000000;
260
+ --color-primary: var(--apple-primary);
261
+ --color-primary-hover: --alpha(var(--apple-primary) / 85%);
262
+ --color-primary-surface: #FFFFFF;
263
+ --color-border: var(--apple-primary);
264
+ }
265
+
266
+ @variant dark {
267
+ .fui-provider__button[data-provider="apple.com"][data-themed="true"] {
268
+ --apple-primary: var(--color-white);
269
+ --color-primary: var(--apple-primary);
270
+ --color-primary-hover: --alpha(var(--apple-primary) / 85%);
271
+ --color-primary-surface: var(--color-black);
272
+ --color-border: var(--color-white);
273
+ }
274
+ }
275
+
276
+ .fui-provider__button[data-provider="github.com"][data-themed="true"] {
277
+ --github-primary: #000000;
278
+ --color-primary: var(--github-primary);
279
+ --color-primary-hover: --alpha(var(--github-primary) / 85%);
280
+ --color-primary-surface: #FFFFFF;
281
+ --color-border: var(--github-primary);
282
+ }
283
+
284
+ @variant dark {
285
+ .fui-provider__button[data-provider="github.com"][data-themed="true"] {
286
+ --github-primary: var(--color-white);
287
+ --color-primary: var(--github-primary);
288
+ --color-primary-hover: --alpha(var(--github-primary) / 85%);
289
+ --color-primary-surface: var(--color-black);
290
+ --color-border: var(--color-white);
291
+ }
292
+ }
293
+
294
+ .fui-provider__button[data-provider="microsoft.com"][data-themed="true"] {
295
+ --microsoft-primary: #2F2F2F;
296
+ --color-primary: var(--microsoft-primary);
297
+ --color-primary-hover: --alpha(var(--microsoft-primary) / 85%);
298
+ --color-primary-surface: #FFFFFF;
299
+ --color-border: var(--microsoft-primary);
300
+ }
301
+
302
+ @variant dark {
303
+ .fui-provider__button[data-provider="microsoft.com"][data-themed="true"] {
304
+ --microsoft-primary: var(--color-white);
305
+ --color-primary: var(--microsoft-primary);
306
+ --color-primary-hover: --alpha(var(--microsoft-primary) / 85%);
307
+ --color-primary-surface: #5E5E5E;
308
+ --color-border: var(--color-white);
309
+ }
310
+ }
311
+
312
+ .fui-provider__button[data-provider="twitter.com"][data-themed="true"] {
313
+ --twitter-primary: #1DA1F2;
314
+ --color-primary: var(--twitter-primary);
315
+ --color-primary-hover: --alpha(var(--twitter-primary) / 85%);
316
+ --color-primary-surface: #FFFFFF;
317
+ --color-border: var(--twitter-primary);
318
+ }
319
+ }
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Copyright 2025 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
18
+
19
+ /**
20
+ * CSS Specificity Test Suite
21
+ *
22
+ * This test suite verifies that our :where() pseudo-class implementation
23
+ * correctly reduces CSS specificity, allowing users to easily override
24
+ * component styles with their own Tailwind classes.
25
+ */
26
+
27
+ describe("Tailwind :where() pseudo-class", () => {
28
+ let testContainer: HTMLDivElement;
29
+ let styleElement: HTMLStyleElement;
30
+
31
+ beforeEach(() => {
32
+ // Create a test container
33
+ testContainer = document.createElement("div");
34
+ document.body.appendChild(testContainer);
35
+
36
+ // Create a style element to inject our CSS
37
+ styleElement = document.createElement("style");
38
+ document.head.appendChild(styleElement);
39
+ });
40
+
41
+ afterEach(() => {
42
+ // Clean up
43
+ document.body.removeChild(testContainer);
44
+ document.head.removeChild(styleElement);
45
+ });
46
+
47
+ /**
48
+ * Helper function to get computed styles for an element
49
+ */
50
+ function getComputedStyleValue(element: HTMLElement, property: string): string {
51
+ return window.getComputedStyle(element).getPropertyValue(property);
52
+ }
53
+
54
+ /**
55
+ * Helper function to create a test element with specific classes
56
+ */
57
+ function createTestElement(classes: string): HTMLElement {
58
+ const element = document.createElement("div");
59
+ element.className = classes;
60
+ testContainer.appendChild(element);
61
+ return element;
62
+ }
63
+
64
+ describe("Basic :where() specificity test", () => {
65
+ it("should demonstrate that :where() has zero specificity", () => {
66
+ // Create CSS that shows specificity difference
67
+ styleElement.textContent = `
68
+ /* Regular class selector (specificity: 0,0,1,0) */
69
+ .regular-class {
70
+ color: red;
71
+ }
72
+
73
+ /* :where() pseudo-class (specificity: 0,0,0,0) */
74
+ :where(.where-class) {
75
+ color: blue;
76
+ }
77
+
78
+ /* User class (specificity: 0,0,1,0) */
79
+ .user-override {
80
+ color: green;
81
+ }
82
+ `;
83
+
84
+ // Test regular class vs user override
85
+ const regularElement = createTestElement("regular-class user-override");
86
+ expect(getComputedStyleValue(regularElement, "color")).toBe("rgb(0, 128, 0)"); // green wins
87
+
88
+ // Test :where() class vs user override
89
+ const whereElement = createTestElement("where-class user-override");
90
+ expect(getComputedStyleValue(whereElement, "color")).toBe("rgb(0, 128, 0)"); // green wins easily
91
+ });
92
+
93
+ it("should allow user classes to override :where() styles", () => {
94
+ // Inject CSS with :where() pseudo-class
95
+ styleElement.textContent = `
96
+ :where(.fui-screen) {
97
+ padding-top: 6rem;
98
+ max-width: 28rem;
99
+ margin-left: auto;
100
+ margin-right: auto;
101
+ }
102
+
103
+ /* User override classes */
104
+ .pt-32 { padding-top: 8rem !important; }
105
+ .max-w-lg { max-width: 32rem !important; }
106
+ `;
107
+
108
+ // Create element with both fui-screen and user override classes
109
+ const element = createTestElement("fui-screen pt-32 max-w-lg");
110
+
111
+ // User classes should override the :where() styles
112
+ expect(getComputedStyleValue(element, "padding-top")).toBe("8rem"); // pt-32 overrides pt-24
113
+ expect(getComputedStyleValue(element, "max-width")).toBe("32rem"); // max-w-lg overrides max-w-md
114
+ });
115
+
116
+ it("should maintain :where() styles when no user overrides are present", () => {
117
+ styleElement.textContent = `
118
+ :where(.fui-screen) {
119
+ padding-top: 6rem;
120
+ max-width: 28rem;
121
+ margin-left: auto;
122
+ margin-right: auto;
123
+ }
124
+ `;
125
+
126
+ const element = createTestElement("fui-screen");
127
+
128
+ // Should maintain original styles
129
+ expect(getComputedStyleValue(element, "padding-top")).toBe("6rem");
130
+ expect(getComputedStyleValue(element, "max-width")).toBe("28rem");
131
+ });
132
+ });
133
+
134
+ describe("Complex selectors with :where()", () => {
135
+ it("should handle nested selectors with :where() correctly", () => {
136
+ styleElement.textContent = `
137
+ :where(.fui-form fieldset > label > input) {
138
+ border: 1px solid #d1d5db;
139
+ border-radius: 0.25rem;
140
+ padding: 0.5rem;
141
+ font-size: 0.875rem;
142
+ background-color: transparent;
143
+ }
144
+
145
+ /* User override classes */
146
+ .border-red-500 { border-color: rgb(239, 68, 68) !important; }
147
+ .rounded-lg { border-radius: 0.5rem !important; }
148
+ .p-3 { padding: 0.75rem !important; }
149
+ `;
150
+
151
+ // Create nested structure
152
+ const form = document.createElement("div");
153
+ form.className = "fui-form";
154
+ const fieldset = document.createElement("fieldset");
155
+ const label = document.createElement("label");
156
+ const input = document.createElement("input");
157
+
158
+ fieldset.appendChild(label);
159
+ label.appendChild(input);
160
+ form.appendChild(fieldset);
161
+ testContainer.appendChild(form);
162
+
163
+ // Add user override classes
164
+ input.className = "border-red-500 rounded-lg p-3";
165
+
166
+ // User classes should override
167
+ expect(getComputedStyleValue(input, "border-color")).toBe("rgb(239, 68, 68)"); // border-red-500
168
+ expect(getComputedStyleValue(input, "border-radius")).toBe("0.5rem"); // rounded-lg
169
+ expect(getComputedStyleValue(input, "padding")).toBe("0.75rem"); // p-3
170
+ });
171
+ });
172
+
173
+ describe("Edge cases", () => {
174
+ it("should handle empty :where() selectors gracefully", () => {
175
+ styleElement.textContent = `
176
+ :where() {
177
+ color: red;
178
+ }
179
+ `;
180
+
181
+ const element = createTestElement("test-class");
182
+ // Should not apply any styles (empty selector)
183
+ expect(getComputedStyleValue(element, "color")).not.toBe("rgb(255, 0, 0)");
184
+ });
185
+
186
+ it("should handle :where() with attribute selectors", () => {
187
+ styleElement.textContent = `
188
+ :where(.fui-form fieldset > label > input[aria-invalid="true"]) {
189
+ outline: 2px solid red;
190
+ }
191
+
192
+ /* User override classes */
193
+ .outline-blue-500 { outline-color: rgb(59, 130, 246) !important; }
194
+ .outline-4 { outline-width: 4px !important; }
195
+ `;
196
+
197
+ const form = document.createElement("div");
198
+ form.className = "fui-form";
199
+ const fieldset = document.createElement("fieldset");
200
+ const label = document.createElement("label");
201
+ const input = document.createElement("input");
202
+ input.setAttribute("aria-invalid", "true");
203
+
204
+ fieldset.appendChild(label);
205
+ label.appendChild(input);
206
+ form.appendChild(fieldset);
207
+ testContainer.appendChild(form);
208
+
209
+ // Add user override
210
+ input.className = "outline-blue-500 outline-4";
211
+
212
+ // User classes should override
213
+ expect(getComputedStyleValue(input, "outline-color")).toBe("rgb(59, 130, 246)"); // outline-blue-500
214
+ expect(getComputedStyleValue(input, "outline-width")).toBe("4px"); // outline-4
215
+ });
216
+ });
217
+
218
+ describe("Component integration tests", () => {
219
+ it("should verify layout components (screen, card) styles can be overridden", () => {
220
+ styleElement.textContent = `
221
+ /* Base component styles with :where() for zero specificity */
222
+ :where(.fui-screen) { padding-top: 6rem; max-width: 28rem; }
223
+ :where(.fui-card) { background-color: white; padding: 2.5rem; }
224
+
225
+ /* User Tailwind override classes */
226
+ .pt-8 { padding-top: 2rem !important; }
227
+ .max-w-lg { max-width: 32rem !important; }
228
+ .p-6 { padding: 1.5rem !important; }
229
+ .bg-gray-50 { background-color: rgb(249, 250, 251) !important; }
230
+ `;
231
+
232
+ const screenElement = createTestElement("fui-screen pt-8 max-w-lg");
233
+ const cardElement = createTestElement("fui-card p-6 bg-gray-50");
234
+
235
+ expect(getComputedStyleValue(screenElement, "padding-top")).toBe("2rem");
236
+ expect(getComputedStyleValue(cardElement, "background-color")).toBe("rgb(249, 250, 251)");
237
+ });
238
+
239
+ it("should verify button components styles can be overridden", () => {
240
+ styleElement.textContent = `
241
+ /* Base component styles with :where() for zero specificity */
242
+ :where(.fui-button) { background-color: black; color: white; padding: 0.5rem 1rem; }
243
+ :where(.fui-button--secondary) { background-color: transparent; border: 1px solid rgb(209, 213, 219); }
244
+
245
+ /* User Tailwind override classes */
246
+ .bg-blue-500 { background-color: rgb(59, 130, 246) !important; }
247
+ .px-6 { padding-left: 1.5rem !important; padding-right: 1.5rem !important; }
248
+ .bg-gray-100 { background-color: rgb(243, 244, 246) !important; }
249
+ `;
250
+
251
+ const primaryButton = createTestElement("fui-button bg-blue-500 px-6");
252
+ const secondaryButton = createTestElement("fui-button--secondary bg-gray-100");
253
+
254
+ expect(getComputedStyleValue(primaryButton, "background-color")).toBe("rgb(59, 130, 246)");
255
+ expect(getComputedStyleValue(secondaryButton, "background-color")).toBe("rgb(243, 244, 246)");
256
+ });
257
+
258
+ it("should verify form components styles can be overridden", () => {
259
+ styleElement.textContent = `
260
+ /* Base component styles with :where() for zero specificity */
261
+ :where(.fui-form fieldset > label > input) { border: 1px solid rgb(209, 213, 219); padding: 0.5rem; }
262
+ :where(.fui-form .fui-form__error) { color: rgb(239, 68, 68); }
263
+
264
+ /* User Tailwind override classes */
265
+ .border-gray-300 { border-color: rgb(209, 213, 219) !important; }
266
+ .px-3 { padding-left: 0.75rem !important; padding-right: 0.75rem !important; }
267
+ .text-red-600 { color: rgb(220, 38, 38) !important; }
268
+ `;
269
+
270
+ const inputElement = createTestElement("border-gray-300 px-3");
271
+ const errorElement = createTestElement("fui-form__error text-red-600");
272
+
273
+ expect(getComputedStyleValue(inputElement, "border-color")).toBe("rgb(209, 213, 219)");
274
+ expect(getComputedStyleValue(errorElement, "color")).toBe("rgb(220, 38, 38)");
275
+ });
276
+
277
+ it("should verify utility components (divider, success) styles can be overridden", () => {
278
+ styleElement.textContent = `
279
+ /* Base component styles with :where() for zero specificity */
280
+ :where(.fui-divider) { display: flex; gap: 0.75rem; }
281
+ :where(.fui-divider__line) { background-color: rgb(229, 231, 235); }
282
+ :where(.fui-success) { text-align: center; font-size: 0.75rem; }
283
+
284
+ /* User Tailwind override classes */
285
+ .gap-2 { gap: 0.5rem !important; }
286
+ .bg-gray-300 { background-color: rgb(209, 213, 219) !important; }
287
+ .text-left { text-align: left !important; }
288
+ .text-sm { font-size: 0.875rem !important; }
289
+ `;
290
+
291
+ const dividerElement = createTestElement("fui-divider gap-2");
292
+ const lineElement = createTestElement("fui-divider__line bg-gray-300");
293
+ const successElement = createTestElement("fui-success text-left text-sm");
294
+
295
+ expect(getComputedStyleValue(dividerElement, "gap")).toBe("0.5rem");
296
+ expect(getComputedStyleValue(lineElement, "background-color")).toBe("rgb(209, 213, 219)");
297
+ expect(getComputedStyleValue(successElement, "text-align")).toBe("left");
298
+ });
299
+
300
+ it("should verify complex input components (phone, country) styles can be overridden", () => {
301
+ styleElement.textContent = `
302
+ /* Base component styles with :where() for zero specificity */
303
+ :where(.fui-phone-input) { display: flex; gap: 0.5rem; }
304
+ :where(.fui-country-selector) { width: 80px; }
305
+
306
+ /* User Tailwind override classes */
307
+ .gap-2 { gap: 0.5rem !important; }
308
+ .w-24 { width: 6rem !important; }
309
+ .bg-gray-50 { background-color: rgb(249, 250, 251) !important; }
310
+ `;
311
+
312
+ const phoneElement = createTestElement("fui-phone-input gap-2");
313
+ const countryElement = createTestElement("fui-country-selector w-24 bg-gray-50");
314
+
315
+ expect(getComputedStyleValue(phoneElement, "gap")).toBe("0.5rem");
316
+ expect(getComputedStyleValue(countryElement, "width")).toBe("6rem");
317
+ });
318
+ });
319
+ });
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Copyright 2025 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE/2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { describe, it, expect } from "vitest";
18
+ import { buttonVariant, type ButtonVariant } from "./index";
19
+
20
+ describe("buttonVariant", () => {
21
+ it("should return base class when no variant is provided", () => {
22
+ const result = buttonVariant();
23
+ expect(result).toBe("fui-button");
24
+ });
25
+
26
+ it("should return base class with primary variant (default)", () => {
27
+ const result = buttonVariant({ variant: "primary" });
28
+ expect(result).toBe("fui-button");
29
+ });
30
+
31
+ it("should return base class with secondary variant", () => {
32
+ const result = buttonVariant({ variant: "secondary" });
33
+ expect(result).toBe("fui-button fui-button--secondary");
34
+ });
35
+
36
+ it("should handle empty variant object", () => {
37
+ const result = buttonVariant({});
38
+ expect(result).toBe("fui-button");
39
+ });
40
+
41
+ it("should handle undefined variant", () => {
42
+ const result = buttonVariant({ variant: undefined });
43
+ expect(result).toBe("fui-button");
44
+ });
45
+ });
46
+
47
+ describe("ButtonVariant type", () => {
48
+ it("should accept valid variant values", () => {
49
+ const primaryVariant: ButtonVariant = "primary";
50
+ const secondaryVariant: ButtonVariant = "secondary";
51
+
52
+ expect(primaryVariant).toBe("primary");
53
+ expect(secondaryVariant).toBe("secondary");
54
+ });
55
+
56
+ it("should work with buttonVariant function", () => {
57
+ const variants: ButtonVariant[] = ["primary", "secondary"];
58
+
59
+ variants.forEach((variant) => {
60
+ const result = buttonVariant({ variant });
61
+ expect(typeof result).toBe("string");
62
+ expect(result).toContain("fui-button");
63
+ });
64
+ });
65
+ });
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { cva, type VariantProps } from "cva";
2
+
3
+ export const buttonVariant = cva({
4
+ base: "fui-button",
5
+ variants: {
6
+ variant: {
7
+ primary: "",
8
+ secondary: "fui-button--secondary",
9
+ },
10
+ },
11
+ defaultVariants: {
12
+ variant: "primary",
13
+ },
14
+ });
15
+
16
+ export type ButtonVariant = VariantProps<typeof buttonVariant>["variant"];
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Copyright 2025 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ @layer theme {
18
+ :root {
19
+ --fui-primary: black;
20
+ --fui-primary-surface: yellow;
21
+ --fui-text: black;
22
+ --fui-text-muted: black;
23
+ --fui-background: yellow;
24
+ --fui-border: green;
25
+ --fui-input: green;
26
+ --fui-error: blue;
27
+ --fui-radius: 0;
28
+ --fui-radius-card: 0;
29
+ }
30
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Copyright 2025 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ @layer theme {
18
+ :root {
19
+ --fui-primary: var(--color-white);
20
+ --fui-primary-hover: --alpha(var(--fui-primary) / 85%);
21
+ --fui-primary-surface: var(--color-black);
22
+ --fui-text: var(--color-white);
23
+ --fui-text-muted: var(--color-gray-200);
24
+ --fui-background: var(--color-black);
25
+ --fui-border: var(--color-gray-200);
26
+ --fui-input: var(--color-gray-300);
27
+ --fui-error: var(--color-red-500);
28
+ --fui-radius: var(--radius-sm);
29
+ --fui-radius-card: var(--radius-xl);
30
+ }
31
+ }