feat: add base app

This commit is contained in:
Kasper Juul Hermansen 2025-01-11 22:45:44 +01:00
commit 11f8cccde4
Signed by: kjuulh
SSH Key Fingerprint: SHA256:RjXh0p7U6opxnfd3ga/Y9TCo18FYlHFdSpRIV72S/QM
30 changed files with 4317 additions and 0 deletions

2
.drone.yml Normal file
View File

@ -0,0 +1,2 @@
kind: template
load: cuddle-rust-leptos-plan.yaml

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
/target/
pkg
**/*.rs.bk
node_modules/
test-results/
end2end/playwright-report/
playwright/.cache/
.cuddle/

3542
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

105
Cargo.toml Normal file
View File

@ -0,0 +1,105 @@
[workspace]
resolver = "2"
members = ["crates/*"]
# need to be applied only to wasm build
[profile.release]
codegen-units = 1
lto = true
opt-level = 'z'
[workspace.dependencies]
leptos = { version = "0.7.2", features = ["nightly"] }
leptos_meta = { version = "0.7.2" }
leptos_router = { version = "0.7.2", features = ["nightly"] }
leptos_axum = { version = "0.7.2" }
server_fn = { version = "0.7.2", features = [] }
serde = { version = "1", features = ["derive"] }
axum = "0.7"
cfg-if = "1"
console_error_panic_hook = "0.1.7"
console_log = "1"
http = "1"
log = "0.4.21"
simple_logger = "5.0.0"
thiserror = "2"
tokio = { version = "1.37.0", features = ["full"] }
tower = { version = "0.5.0", features = ["full"] }
tower-http = { version = "0.6", features = ["full"] }
wasm-bindgen = "=0.2.99"
# See https://github.com/akesson/cargo-leptos for documentation of all the parameters.
# A leptos project defines which workspace members
# that are used together frontend (lib) & server (bin)
[[workspace.metadata.leptos]]
# this name is used for the wasm, js and css file names
name = "client"
# the package in the workspace that contains the server binary (binary crate)
bin-package = "client"
# the package in the workspace that contains the frontend wasm binary (library crate)
lib-package = "frontend"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "style/main.scss"
# Assets source dir. All files found here will be copied and synchronized to site-root.
# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir.
#
# Optional. Env: LEPTOS_ASSETS_DIR.
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# Optional, Activates the tailwind build
tailwind-input-file = "style/tailwind.css"
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
# [Windows] for non-WSL use "npx.cmd playwright test"
# This binary name can be checked in Powershell with Get-Command npx
end2end-cmd = "npx playwright test"
end2end-dir = "end2end"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = []
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = []
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false
frontend-files-content-hashes = false

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 henrik
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

1
README.md Normal file
View File

@ -0,0 +1 @@
# client

37
crates/app/Cargo.toml Normal file
View File

@ -0,0 +1,37 @@
[package]
name = "app"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
leptos.workspace = true
leptos_meta.workspace = true
leptos_router.workspace = true
server_fn.workspace = true
axum = { workspace = true, optional = true, features = ["macros"] }
leptos_axum = { workspace = true, optional = true }
#tokio = { workspace = true, optional = true }
serde.workspace = true
http.workspace = true
cfg-if.workspace = true
thiserror.workspace = true
reqwest = { version = "0.12.11", optional = true, features = ["json"] }
serde_json = { version = "1.0.134", optional = true }
[features]
default = []
hydrate = ["leptos/hydrate"]
ssr = [
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:leptos_axum",
"dep:serde_json",
"dep:axum",
]
reqwest = ["dep:reqwest"]
axum = ["dep:axum"]
serde_json = ["dep:serde_json"]

View File

@ -0,0 +1,74 @@
use cfg_if::cfg_if;
use http::status::StatusCode;
use leptos::prelude::*;
use thiserror::Error;
#[cfg(feature = "ssr")]
use leptos_axum::ResponseOptions;
#[derive(Clone, Debug, Error)]
pub enum AppError {
#[error("Not Found")]
NotFound,
}
impl AppError {
pub fn status_code(&self) -> StatusCode {
match self {
AppError::NotFound => StatusCode::NOT_FOUND,
}
}
}
// A basic function to display errors served by the error boundaries.
// Feel free to do more complicated things here than just displaying the error.
#[component]
pub fn ErrorTemplate(
#[prop(optional)] outside_errors: Option<Errors>,
#[prop(optional)] errors: Option<RwSignal<Errors>>,
) -> impl IntoView {
let errors = match outside_errors {
Some(e) => RwSignal::new(e),
None => match errors {
Some(e) => e,
None => panic!("No Errors found and we expected errors!"),
},
};
// Get Errors from Signal
let errors = errors.get_untracked();
// Downcast lets us take a type that implements `std::error::Error`
let errors: Vec<AppError> = errors
.into_iter()
.filter_map(|(_k, v)| v.downcast_ref::<AppError>().cloned())
.collect();
println!("Errors: {errors:#?}");
// Only the response code for the first error is actually sent from the server
// this may be customized by the specific application
cfg_if! { if #[cfg(feature="ssr")] {
let response = use_context::<ResponseOptions>();
if let Some(response) = response {
response.set_status(errors[0].status_code());
}
}}
view! {
<h1>{if errors.len() > 1 { "Errors" } else { "Error" }}</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
each=move || { errors.clone().into_iter().enumerate() }
// a unique key for each item as a reference
key=|(index, _error)| *index
// renders each item to a view
children=move |error| {
let error_string = error.1.to_string();
let error_code = error.1.status_code();
view! {
<h2>{error_code.to_string()}</h2>
<p>"Error: " {error_string}</p>
}
}
/>
}
}

61
crates/app/src/lib.rs Normal file
View File

@ -0,0 +1,61 @@
use crate::error_template::{AppError, ErrorTemplate};
use leptos::prelude::*;
use leptos_meta::*;
use leptos_router::{components::*, StaticSegment};
pub mod error_template;
#[cfg(feature = "ssr")]
pub mod state;
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<AutoReload options=options.clone() />
<HydrationScripts options />
<MetaTags />
</head>
<body>
<App />
</body>
</html>
}
}
#[component]
pub fn App() -> impl IntoView {
// Provides context that manages stylesheets, titles, meta tags, etc.
provide_meta_context();
view! {
<Stylesheet id="leptos" href="/pkg/client.css" />
// sets the document title
<Title text="client" />
// content for this welcome page
<Router>
<main class="">
<Routes fallback=|| {
let mut outside_errors = Errors::default();
outside_errors.insert_with_default_key(AppError::NotFound);
view! { <ErrorTemplate outside_errors /> }.into_view()
}>
<Route path=StaticSegment("") view=HomePage />
</Routes>
</main>
</Router>
}
}
#[component]
pub fn HomePage() -> impl IntoView {
view! {
<h1> "client" </h1>
}
}

14
crates/app/src/state.rs Normal file
View File

@ -0,0 +1,14 @@
use axum::extract::FromRef;
use leptos::prelude::expect_context;
use server_fn::ServerFnError;
#[derive(FromRef, Clone)]
pub struct State {}
pub async fn get_state() -> Result<State, ServerFnError> {
let state = expect_context::<crate::state::State>();
let axum::extract::State(state): axum::extract::State<crate::state::State> =
leptos_axum::extract_with_state(&state).await?;
Ok(state)
}

19
crates/client/Cargo.toml Normal file
View File

@ -0,0 +1,19 @@
[package]
name = "client"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
app = { path = "../app", default-features = false, features = ["ssr"] }
leptos = { workspace = true, features = ["ssr"] }
leptos_axum.workspace = true
axum.workspace = true
simple_logger.workspace = true
tokio.workspace = true
tower.workspace = true
tower-http.workspace = true
log.workspace = true
dotenvy = "0.15.7"

78
crates/client/src/main.rs Normal file
View File

@ -0,0 +1,78 @@
use app::*;
use axum::Router;
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use state::State;
use tokio::signal;
#[tokio::main]
async fn main() {
let _ = dotenvy::dotenv();
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
// Setting get_configuration(None) means we'll be using cargo-leptos's env values
// For deployment these variables are:
// <https://github.com/leptos-rs/start-axum#executing-a-server-on-a-remote-machine-without-the-toolchain>
// Alternately a file can be specified such as Some("Cargo.toml")
// The file would need to be included with the executable when moved to deployment
let conf = get_configuration(None).unwrap();
let leptos_options = conf.leptos_options;
let state = State {};
let addr = leptos_options.site_addr;
let routes = generate_route_list(App);
// build our application with a route
let app = Router::new()
.leptos_routes_with_context(
&leptos_options,
routes,
{
let state = state.clone();
move || {
provide_context(state.clone());
}
},
{
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
},
)
.fallback(leptos_axum::file_and_error_handler(shell))
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log::info!("listening on http://{}", &addr);
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.with_graceful_shutdown(shutdown_signal())
.await
.unwrap();
}
async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("failed to install signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
}

View File

@ -0,0 +1,18 @@
[package]
name = "frontend"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
app = { path = "../app", default-features = false, features = ["hydrate"] }
leptos = { workspace = true, features = [ "hydrate" ] }
console_error_panic_hook.workspace = true
console_log.workspace = true
log.workspace = true
wasm-bindgen.workspace = true

View File

@ -0,0 +1,10 @@
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use app::*;
// initializes logging using the `log` crate
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount::hydrate_body(App);
}

31
cuddle.yaml Normal file
View File

@ -0,0 +1,31 @@
# yaml-language-server: $schema=https://git.front.kjuulh.io/kjuulh/cuddle/raw/branch/main/schemas/base.json
base: "git@git.front.kjuulh.io:kjuulh/cuddle-rust-leptos-plan.git"
vars:
service: "client"
registry: kasperhermansen
ingress:
- external: "true"
- internal: "true"
cuddle/clusters:
dev:
env:
service.host: "0.0.0.0:3000"
content.url: https://content.front.kjuulh.io/graphql
content.base.url: https://content.front.kjuulh.io
content.token:
vault: true
prod:
env:
service.host: "0.0.0.0:3000"
content.url: https://content.front.kjuulh.io/graphql
content.base.url: https://content.front.kjuulh.io
content.token:
vault: true
scripts:
generate_graphql:
type: shell

109
end2end/package-lock.json generated Normal file
View File

@ -0,0 +1,109 @@
{
"name": "end2end",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "end2end",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.28.0"
}
},
"node_modules/@playwright/test": {
"version": "1.49.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz",
"integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==",
"dev": true,
"dependencies": {
"playwright": "1.49.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.49.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz",
"integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==",
"dev": true,
"dependencies": {
"playwright-core": "1.49.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.49.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz",
"integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
}
},
"dependencies": {
"@playwright/test": {
"version": "1.49.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz",
"integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==",
"dev": true,
"requires": {
"playwright": "1.49.1"
}
},
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true
},
"playwright": {
"version": "1.49.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz",
"integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==",
"dev": true,
"requires": {
"fsevents": "2.3.2",
"playwright-core": "1.49.1"
}
},
"playwright-core": {
"version": "1.49.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz",
"integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==",
"dev": true
}
}
}

13
end2end/package.json Normal file
View File

@ -0,0 +1,13 @@
{
"name": "end2end",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.28.0"
}
}

View File

@ -0,0 +1,107 @@
import type { PlaywrightTestConfig } from "@playwright/test";
import { devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig = {
testDir: "./tests",
/* Maximum time one test can run for. */
timeout: 30 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 5000,
},
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
},
},
{
name: "firefox",
use: {
...devices["Desktop Firefox"],
},
},
{
name: "webkit",
use: {
...devices["Desktop Safari"],
},
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: {
// ...devices['Pixel 5'],
// },
// },
// {
// name: 'Mobile Safari',
// use: {
// ...devices['iPhone 12'],
// },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: {
// channel: 'msedge',
// },
// },
// {
// name: 'Google Chrome',
// use: {
// channel: 'chrome',
// },
// },
],
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
// outputDir: 'test-results/',
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// port: 3000,
// },
};
export default config;

View File

@ -0,0 +1,9 @@
import { test, expect } from "@playwright/test";
test("homepage has title and links to intro page", async ({ page }) => {
await page.goto("http://localhost:3000/");
await expect(page).toHaveTitle("Welcome to Leptos");
await expect(page.locator("h1")).toHaveText("Welcome to Leptos!");
});

12
leptosfmt.toml Normal file
View File

@ -0,0 +1,12 @@
max_width = 100 # Maximum width of each line
tab_spaces = 4 # Number of spaces per tab
indentation_style = "Auto" # "Tabs", "Spaces" or "Auto"
newline_style = "Auto" # "Unix", "Windows" or "Auto"
attr_value_brace_style = "WhenRequired" # "Always", "AlwaysUnlessLit", "WhenRequired" or "Preserve"
macro_names = ["leptos::view", "view"] # Macro names which will be formatted
closing_tag_style = "Preserve" # "Preserve", "SelfClosing" or "NonSelfClosing"
# Attribute values can be formatted by custom formatters
# Every attribute name may only select one formatter (this might change later on)
[attr_values]
class = "Tailwind" # "Tailwind" is the only attribute value formatter available for now

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
public/profile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

3
renovate.json Normal file
View File

@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

2
rust-analyzer.toml Normal file
View File

@ -0,0 +1,2 @@
[rustfmt]
overrideCommand = ["leptosfmt", "--stdin", "--rustfmt"]

2
rust-toolchain.toml Normal file
View File

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

1
rustfmt.toml Normal file
View File

@ -0,0 +1 @@
edition = "2021"

View File

@ -0,0 +1,5 @@
#!/usr/bin/env zsh
set -e
graphql-schema download --endpoint http://localhost:1337/graphql --output-file crates/app/graphql/content-schema.graphql

11
style/main.scss Normal file
View File

@ -0,0 +1,11 @@
:root {
--main-font: sans-serif;
--main-background: #201D25;
--main-color: #E1CECE;
}
body {
font-family: var(--main-font);
//background-color: var(--main-background);
//color: var(--main-color);
}

7
style/tailwind.css Normal file
View File

@ -0,0 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.page-center {
@apply max-w-4xl mx-auto px-4 h-full;
}

13
tailwind.config.js Normal file
View File

@ -0,0 +1,13 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: {
files: ["*.html", "./crates/app/src/**/*.rs"],
transform: {
rs: (content) => content.replace(/(?:^|\s)class:/g, ' '),
},
},
theme: {
extend: {},
},
plugins: [],
}