mirror of
https://github.com/gradio-app/gradio.git
synced 2025-02-23 11:39:17 +08:00
Allow applying @media
, @keyframes
and @import
in custom CSS (#7395)
* allow @media and @keyframes in custom CSS * add changeset * demo tweak * formatting * fix * tweak * add .dark test * formatting * add font-face test * support @import statements * add changeset * tweak * fix test --------- Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
This commit is contained in:
parent
fa8225d24d
commit
46b45683e1
6
.changeset/true-beans-flow.md
Normal file
6
.changeset/true-beans-flow.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
"@gradio/app": patch
|
||||
"gradio": patch
|
||||
---
|
||||
|
||||
fix:Allow applying `@media`, `@keyframes` and `@import` in custom CSS
|
1
demo/custom_css/run.ipynb
Normal file
1
demo/custom_css/run.ipynb
Normal file
@ -0,0 +1 @@
|
||||
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: custom_css"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "\n", "css = \"\"\"\n", "/* CSSKeyframesRule for animation */\n", "@keyframes animation {\n", " from {background-color: red;}\n", " to {background-color: blue;}\n", "}\n", "\n", ".cool-col {\n", " animation-name: animation;\n", " animation-duration: 4s;\n", " animation-iteration-count: infinite;\n", " border-radius: 10px;\n", " padding: 20px;\n", "}\n", "\n", "/* CSSStyleRule */\n", ".markdown {\n", " background-color: lightblue;\n", " padding: 20px;\n", "}\n", "\n", ".markdown p {\n", " color: royalblue;\n", "}\n", "\n", "/* CSSMediaRule */\n", "@media screen and (max-width: 600px) {\n", " .markdown {\n", " background: blue;\n", " }\n", " .markdown p {\n", " color: lightblue;\n", " }\n", "}\n", "\n", ".dark .markdown {\n", " background: pink;\n", "}\n", "\n", ".darktest h3 {\n", " color: black;\n", "}\n", "\n", ".dark .darktest h3 {\n", " color: yellow;\n", "}\n", "\n", "/* CSSFontFaceRule */\n", "@font-face {\n", " font-family: \"test-font\";\n", " src: url(\"https://mdn.github.io/css-examples/web-fonts/VeraSeBd.ttf\") format(\"truetype\");\n", "}\n", "\n", ".cool-col {\n", " font-family: \"test-font\";\n", "}\n", "\n", "/* CSSImportRule */\n", "@import url(\"https://fonts.googleapis.com/css2?family=Protest+Riot&display=swap\");\n", "\n", ".markdown {\n", " font-family: \"Protest Riot\", sans-serif;\n", "}\n", "\"\"\"\n", "\n", "with gr.Blocks(css=css) as demo:\n", " with gr.Column(elem_classes=\"cool-col\"):\n", " gr.Markdown(\"### Gradio Demo with Custom CSS\", elem_classes=\"darktest\")\n", " gr.Markdown(elem_classes=\"markdown\", value=\"Resize the browser window to see the CSS media query in action.\")\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
|
74
demo/custom_css/run.py
Normal file
74
demo/custom_css/run.py
Normal file
@ -0,0 +1,74 @@
|
||||
import gradio as gr
|
||||
|
||||
css = """
|
||||
/* CSSKeyframesRule for animation */
|
||||
@keyframes animation {
|
||||
from {background-color: red;}
|
||||
to {background-color: blue;}
|
||||
}
|
||||
|
||||
.cool-col {
|
||||
animation-name: animation;
|
||||
animation-duration: 4s;
|
||||
animation-iteration-count: infinite;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* CSSStyleRule */
|
||||
.markdown {
|
||||
background-color: lightblue;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.markdown p {
|
||||
color: royalblue;
|
||||
}
|
||||
|
||||
/* CSSMediaRule */
|
||||
@media screen and (max-width: 600px) {
|
||||
.markdown {
|
||||
background: blue;
|
||||
}
|
||||
.markdown p {
|
||||
color: lightblue;
|
||||
}
|
||||
}
|
||||
|
||||
.dark .markdown {
|
||||
background: pink;
|
||||
}
|
||||
|
||||
.darktest h3 {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.dark .darktest h3 {
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
/* CSSFontFaceRule */
|
||||
@font-face {
|
||||
font-family: "test-font";
|
||||
src: url("https://mdn.github.io/css-examples/web-fonts/VeraSeBd.ttf") format("truetype");
|
||||
}
|
||||
|
||||
.cool-col {
|
||||
font-family: "test-font";
|
||||
}
|
||||
|
||||
/* CSSImportRule */
|
||||
@import url("https://fonts.googleapis.com/css2?family=Protest+Riot&display=swap");
|
||||
|
||||
.markdown {
|
||||
font-family: "Protest Riot", sans-serif;
|
||||
}
|
||||
"""
|
||||
|
||||
with gr.Blocks(css=css) as demo:
|
||||
with gr.Column(elem_classes="cool-col"):
|
||||
gr.Markdown("### Gradio Demo with Custom CSS", elem_classes="darktest")
|
||||
gr.Markdown(elem_classes="markdown", value="Resize the browser window to see the CSS media query in action.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
demo.launch()
|
@ -29,9 +29,16 @@ export function prefix_css(
|
||||
const stylesheet = new CSSStyleSheet();
|
||||
stylesheet.replaceSync(string);
|
||||
|
||||
let importString = "";
|
||||
string = string.replace(/@import\s+url\((.*?)\);\s*/g, (match, url) => {
|
||||
importString += `@import url(${url});\n`;
|
||||
return ""; // remove and store any @import statements from the CSS
|
||||
});
|
||||
|
||||
const rules = stylesheet.cssRules;
|
||||
|
||||
let css_string = "";
|
||||
let gradio_css_infix = `gradio-app .gradio-container.gradio-container-${version} .contain `;
|
||||
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
const rule = rules[i];
|
||||
@ -45,17 +52,49 @@ export function prefix_css(
|
||||
.split(",")
|
||||
.map(
|
||||
(s) =>
|
||||
`${
|
||||
is_dark_rule ? ".dark" : ""
|
||||
} gradio-app .gradio-container.gradio-container-${version} .contain ${s.trim()} `
|
||||
`${is_dark_rule ? ".dark" : ""} ${gradio_css_infix} ${s.trim()} `
|
||||
)
|
||||
.join(",");
|
||||
|
||||
css_string += rule.cssText;
|
||||
css_string += rule.cssText.replace(selector, new_selector);
|
||||
}
|
||||
} else if (rule instanceof CSSMediaRule) {
|
||||
let mediaCssString = `@media ${rule.media.mediaText} {`;
|
||||
for (let j = 0; j < rule.cssRules.length; j++) {
|
||||
const innerRule = rule.cssRules[j];
|
||||
if (innerRule instanceof CSSStyleRule) {
|
||||
let is_dark_rule = innerRule.cssText.includes(".dark ");
|
||||
const selector = innerRule.selectorText;
|
||||
const new_selector = selector
|
||||
.replace(".dark", "")
|
||||
.split(",")
|
||||
.map(
|
||||
(s) =>
|
||||
`${
|
||||
is_dark_rule ? ".dark" : ""
|
||||
} ${gradio_css_infix} ${s.trim()} `
|
||||
)
|
||||
.join(",");
|
||||
mediaCssString += innerRule.cssText.replace(selector, new_selector);
|
||||
}
|
||||
}
|
||||
mediaCssString += "}";
|
||||
css_string += mediaCssString;
|
||||
} else if (rule instanceof CSSKeyframesRule) {
|
||||
css_string += `@keyframes ${rule.name} {`;
|
||||
for (let j = 0; j < rule.cssRules.length; j++) {
|
||||
const innerRule = rule.cssRules[j];
|
||||
if (innerRule instanceof CSSKeyframeRule) {
|
||||
css_string += `${innerRule.keyText} { ${innerRule.style.cssText} }`;
|
||||
}
|
||||
}
|
||||
css_string += "}";
|
||||
} else if (rule instanceof CSSFontFaceRule) {
|
||||
css_string += `@font-face { ${rule.style.cssText} }`;
|
||||
}
|
||||
}
|
||||
css_string = importString + css_string;
|
||||
style_element.textContent = css_string;
|
||||
|
||||
document.head.appendChild(style_element);
|
||||
|
71
js/app/test/custom_css.spec.ts
Normal file
71
js/app/test/custom_css.spec.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { test, expect } from "@gradio/tootils";
|
||||
|
||||
test("renders the correct elements", async ({ page }) => {
|
||||
await expect(page.getByTestId("markdown")).toHaveCount(2);
|
||||
});
|
||||
|
||||
test("applies the custom CSS styles", async ({ page }) => {
|
||||
// Test for CSSKeyframesRule
|
||||
const animationName = await page
|
||||
.locator(".cool-col")
|
||||
.evaluate((node) => getComputedStyle(node).animationName);
|
||||
expect(animationName).toBe("animation");
|
||||
|
||||
// Test for CSSMediaRule and CSSStyleRule
|
||||
await page.setViewportSize({ width: 500, height: 720 });
|
||||
await expect(page.locator(".markdown").nth(1)).toHaveCSS(
|
||||
"background-color",
|
||||
"rgb(0, 0, 255)"
|
||||
);
|
||||
await expect(page.locator(".markdown p")).toHaveCSS(
|
||||
"color",
|
||||
"rgb(173, 216, 230)"
|
||||
);
|
||||
|
||||
await page.setViewportSize({ width: 1280, height: 720 });
|
||||
await expect(page.locator(".markdown").nth(1)).toHaveCSS(
|
||||
"background-color",
|
||||
"rgb(173, 216, 230)"
|
||||
);
|
||||
await expect(page.locator(".markdown p")).toHaveCSS(
|
||||
"color",
|
||||
"rgb(65, 105, 225)"
|
||||
);
|
||||
});
|
||||
|
||||
test("applies the custom font family", async ({ page }) => {
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Gradio Demo with Custom CSS" })
|
||||
).toHaveCSS("font-family", "test-font");
|
||||
});
|
||||
|
||||
test("applies resources from the @import rule", async ({ page }) => {
|
||||
await expect(page.getByText("Resize the browser window to")).toHaveCSS(
|
||||
"font-family",
|
||||
'"Protest Riot", sans-serif'
|
||||
);
|
||||
});
|
||||
|
||||
test(".dark styles are applied corrently", async ({ page }) => {
|
||||
await page.emulateMedia({ colorScheme: "dark" });
|
||||
|
||||
await expect(page.locator(".markdown").nth(1)).toHaveCSS(
|
||||
"background-color",
|
||||
"rgb(255, 192, 203)"
|
||||
);
|
||||
await expect(page.locator(".darktest h3")).toHaveCSS(
|
||||
"color",
|
||||
"rgb(255, 255, 0)"
|
||||
);
|
||||
|
||||
await page.emulateMedia({ colorScheme: "light" });
|
||||
|
||||
await expect(page.locator(".markdown").nth(1)).toHaveCSS(
|
||||
"background-color",
|
||||
"rgb(173, 216, 230)"
|
||||
);
|
||||
await expect(page.locator(".darktest h3")).toHaveCSS(
|
||||
"color",
|
||||
"rgb(31, 41, 55)"
|
||||
);
|
||||
});
|
Loading…
Reference in New Issue
Block a user