feat: rewrite components/islands css loading with dynamic tree-shake loader

This commit is contained in:
Julien Oculi 2025-05-16 15:24:59 +02:00
parent e7bd691d53
commit d461e53a00
33 changed files with 385 additions and 100 deletions

View file

@ -1,5 +1,5 @@
.components__auto_grid {
display: grid;
gap: var(--_gap);
margin-block: var(--_gap);
:scope {
display: grid;
gap: var(--_gap);
margin-block: var(--_gap);
}

View file

@ -1,7 +1,10 @@
import { ComponentChildren, JSX } from 'preact'
import { getStyleScope, useSmartStylesheet } from ':plugins/SmartStylesheet.tsx'
type Units = 'rem' | '%' | 'px'
const scope = getStyleScope(AutoGrid)
export function AutoGrid(
{ columnWidth, children, style }: {
columnWidth: `${number}${Units}`
@ -9,9 +12,11 @@ export function AutoGrid(
style?: JSX.CSSProperties
},
) {
useSmartStylesheet(import.meta, { scope })
return (
<div
class='components__auto_grid'
class={scope}
style={{
gridTemplateColumns: `repeat(auto-fit, minmax(${columnWidth}, 1fr));`,
...style,

View file

@ -1,5 +1,6 @@
import { Markdown } from ':components/Markdown.tsx'
import { NewsFrontMatter } from ':src/blog/types.ts'
import { useSmartStylesheet } from ':plugins/SmartStylesheet.tsx'
export type BlogProps = {
title: string
@ -19,6 +20,8 @@ export function BlogCard(
{ title, description, author, lastUpdate, name, options, tags, publisher }:
BlogProps,
) {
useSmartStylesheet(import.meta)
return (
<div
class='components__blog_block components__blog_block--card'
@ -55,6 +58,8 @@ export function BlogPost(
publisher,
}: BlogProps,
) {
useSmartStylesheet(import.meta)
return (
<div class='components__blog_block--post'>
<h1>{title}</h1>

View file

@ -1,3 +1,5 @@
import { useSmartStylesheet } from ':plugins/SmartStylesheet.tsx'
export function CohabitInfoTable() {
return (
<div class='components__cohabit_info_table'>
@ -8,6 +10,8 @@ export function CohabitInfoTable() {
}
export function TechnoshopInfoTable() {
useSmartStylesheet(import.meta)
return (
<section class='components__cohabit_info_table__technoshop'>
<div>
@ -76,6 +80,8 @@ export function TechnoshopInfoTable() {
}
export function FablabInfoTable() {
useSmartStylesheet(import.meta)
return (
<section class='components__cohabit_info_table__fablab'>
<div>

View file

@ -1,9 +1,9 @@
footer {
:scope {
height: fit-content;
background-color: var(--_translucent);
}
footer div {
:scope div {
max-width: var(--_wide-screen);
width: 100%;
padding: var(--_gap);
@ -14,13 +14,13 @@ footer div {
}
@media (width < 800px) {
footer div {
:scope div {
flex-wrap: wrap;
text-align: center;
}
}
.footer__badges {
.badges {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--_gap-half);
@ -28,7 +28,7 @@ footer div {
padding-left: 0;
}
.footer__badges__item {
.badges__item {
text-decoration: none;
background-color: var(--_translucent);
color: var(--_font-color);

View file

@ -1,6 +1,12 @@
import { getStyleScope, useSmartStylesheet } from ':plugins/SmartStylesheet.tsx'
const scope = getStyleScope(Footer)
export function Footer() {
useSmartStylesheet(import.meta, { scope })
return (
<footer>
<footer class={scope}>
<div>
<section>
<h3>Nous contacter</h3>
@ -41,9 +47,9 @@ export function Footer() {
</section>
<section>
<h3>Nos réseaux</h3>
<div class='footer__badges'>
<div class='badges'>
<a
class='footer__badges__item'
class='badges__item'
href='https://tube.aquilenet.fr/c/cohabit_fablab/videos'
target='_blank'
title='Peertube'
@ -51,7 +57,7 @@ export function Footer() {
<i class='ri-play-line'></i>
</a>
<a
class='footer__badges__item'
class='badges__item'
href='https://www.facebook.com/people/Fablab-Cohabit/100069798301175/'
target='_blank'
title='Facebook'
@ -59,7 +65,7 @@ export function Footer() {
<i class='ri-facebook-line'></i>
</a>
<a
class='footer__badges__item'
class='badges__item'
href='https://www.instagram.com/fablabcohabit/'
target='_blank'
title='Instagram'
@ -67,7 +73,7 @@ export function Footer() {
<i class='ri-instagram-line'></i>
</a>
<a
class='footer__badges__item'
class='badges__item'
href='https://www.linkedin.com/company/fablab-cohabit/about/'
target='_blank'
title='LinkedIn'
@ -75,7 +81,7 @@ export function Footer() {
<i class='ri-linkedin-line'></i>
</a>
<a
class='footer__badges__item'
class='badges__item'
href='https://toot.aquilenet.fr/@cohabitfablab'
target='_blank'
title='Mastodon'
@ -83,7 +89,7 @@ export function Footer() {
<i class='ri-mastodon-line'></i>
</a>
<a
class='footer__badges__item'
class='badges__item'
href='https://matrix.to/#/!thtlRrlXFrbifqMNCG:matrix.org?via=matrix.org'
target='_blank'
title='Matrix'

View file

@ -1,4 +1,4 @@
header {
:scope {
width: 100%;
padding: 0;
margin: 0;
@ -8,7 +8,7 @@ header {
--_header-width-small-screen: 960px;
}
header div {
:scope div {
max-width: var(--_wide-screen);
margin: auto;
padding: var(--_gap);
@ -16,7 +16,7 @@ header div {
gap: var(--_gap);
}
.components__header__brand {
.brand {
text-wrap: nowrap;
padding: var(--_gap);
font-size: large;
@ -28,7 +28,7 @@ header div {
}
@media (width < var(--_header-width-small-screen)) {
.components__header__brand__text {
.brand__text {
display: none;
}
@ -38,7 +38,7 @@ header div {
}
}
header menu {
:scope menu {
margin: 0;
padding: 0;
width: 100%;
@ -66,7 +66,7 @@ header menu {
}
}
header details {
:scope details {
cursor: pointer;
padding: var(--_gap-half);
@ -152,7 +152,7 @@ header details {
}
}
.components__header__menu_button {
.menu_button {
color: var(--_font-color);
display: inline-block;
text-decoration: none;

View file

@ -3,33 +3,38 @@ import AiChatBox from ':islands/AiChatBox.tsx'
import MoreBox from ':islands/MoreBox.tsx'
import SearchBox from ':islands/SearchBox.tsx'
import ThemePicker from ':islands/ThemePicker.tsx'
import { getStyleScope, useSmartStylesheet } from ':plugins/SmartStylesheet.tsx'
const scope = getStyleScope(Header)
export function Header() {
useSmartStylesheet(import.meta, { scope })
return (
<header>
<header class={scope}>
<div>
<span class='components__header__brand'>
<span class='brand'>
<a href='/'>
<span class='components__header__brand__text'>Coh</span>
<span class='brand__text'>Coh</span>
<img src={asset('/assets/bulb.svg')} alt='a' />
<span class='components__header__brand__text'>bit</span>
<span class='brand__text'>bit</span>
</a>
</span>
<menu>
<li>
<a href='/machines' class='components__header__menu_button'>
<a href='/machines' class='menu_button'>
<i class='ri-hammer-line'></i>
<span>Machines</span>
</a>
</li>
<li>
<a href='/projets' class='components__header__menu_button'>
<a href='/projets' class='menu_button'>
<i class='ri-organization-chart'></i>
<span>Projets</span>
</a>
</li>
<li>
<a href='/blog' class='components__header__menu_button'>
<a href='/blog' class='menu_button'>
<i class='ri-question-answer-line'></i>
<span>Blog</span>
</a>

View file

@ -1,4 +1,4 @@
.components__heros {
:scope {
height: calc(100dvh - 6rem);
display: flex;
flex-direction: column;
@ -7,7 +7,7 @@
gap: calc(2 * var(--_gap));
}
.components__heros__title {
.title {
font-size: 400%;
max-width: 800px;
text-wrap: balance;
@ -15,7 +15,7 @@
margin: 0;
}
.components__heros__subtitle {
.subtitle {
font-size: large;
text-align: center;
text-wrap: balance;

View file

@ -1,10 +1,16 @@
import { getStyleScope, useSmartStylesheet } from ':plugins/SmartStylesheet.tsx'
const scope = getStyleScope(Heros)
export function Heros() {
useSmartStylesheet(import.meta, { scope })
return (
<section class='components__heros'>
<h1 class='components__heros__title'>
<section class={scope}>
<h1 class='title'>
L'atelier participatif libre et open source
</h1>
<p class='components__heros__subtitle'>
<p class='subtitle'>
Venez découvrir un tout nouvelle univers vous pouvez concrétiser vos
projet, rencontrer des passionnés apprendre et développer votre savoir
faire, dans l'entraide et le partage.

View file

@ -1,4 +1,4 @@
.components__machine_card {
:scope {
min-width: 10rem;
aspect-ratio: 1 / 2;
display: flex;
@ -19,11 +19,11 @@
}
}
.components__machine_card__spacer {
.spacer {
height: 50%;
}
.components__machine_card__tags {
.tags {
display: flex;
flex-wrap: wrap;
gap: var(--_gap-half);
@ -34,7 +34,7 @@
}
}
.components__machine_card__footer {
.footer {
height: fit-content;
display: flex;
gap: var(--_gap);

View file

@ -1,3 +1,5 @@
import { getStyleScope, useSmartStylesheet } from ':plugins/SmartStylesheet.tsx'
type MachineCardProps = {
img: string
name: string
@ -6,24 +8,28 @@ type MachineCardProps = {
free: boolean
}
const scope = getStyleScope(MachineCard)
export function MachineCard(
{ name, tags, img, id, free }: MachineCardProps,
) {
useSmartStylesheet(import.meta, { scope })
const stateIcon = free
? <i class='ri-checkbox-circle-line'></i>
: <i class='ri-indeterminate-circle-line'></i>
return (
<div class='components__machine_card' style={{ backgroundImage: img }}>
<div class='components__machine_card__spacer'></div>
<div class={scope} style={{ backgroundImage: img }}>
<div class='spacer'></div>
<h3>
{stateIcon}
<span>{name}</span>
</h3>
<div class='components__machine_card__tags'>
<div class='tags'>
{tags.map((tag, index) => <span key={index}>{tag}</span>)}
</div>
<div class='components__machine_card__footer'>
<div class='footer'>
<a href={`/machines/${id}`}>Infos</a>
<a href={`/machines/${id}`}>Reserver</a>
</div>

View file

@ -1,4 +1,4 @@
.components__member_card {
:scope {
display: grid;
grid-template-rows: 1fr auto;
width: 16rem;
@ -24,13 +24,13 @@
}
}
.components__member_card__icon {
.icon {
height: 100%;
object-fit: cover;
overflow: hidden;
}
.components__member_card__content {
.content {
background-color: var(--_translucent-bg);
backdrop-filter: blur(var(--_blur));
display: flex;
@ -39,7 +39,7 @@
gap: var(--_gap-half);
}
.components__member_card__groups {
.groups {
display: flex;
flex-wrap: wrap;
gap: var(--_gap-half);

View file

@ -1,3 +1,5 @@
import { getStyleScope, useSmartStylesheet } from ':plugins/SmartStylesheet.tsx'
export type MemberCardProps = {
id: string
icon: string
@ -5,17 +7,21 @@ export type MemberCardProps = {
groups: string[]
}
const scope = getStyleScope(MemberCard)
export function MemberCard(
{ id, icon, name, groups }: MemberCardProps,
) {
useSmartStylesheet(import.meta, { scope })
return (
<div class='components__member_card' style={{ backgroundImage: icon }}>
<div class='components__member_card__spacer'></div>
<div class='components__member_card__content'>
<div class={scope} style={{ backgroundImage: icon }}>
<div class='spacer'></div>
<div class='content'>
<h3>
<a href={`/membres/${id}`}>{name}</a>
</h3>
<div class='components__member_card__groups'>
<div class='groups'>
{groups.map((group, index) => <span key={index}>{group}</span>)}
</div>
</div>

View file

@ -1,4 +1,4 @@
.components__project_card {
:scope {
min-width: 30rem;
aspect-ratio: 2 / 1;
display: grid;
@ -33,23 +33,23 @@
}
}
.components__project_card__icon {
.icon {
height: 100%;
object-fit: cover;
overflow: hidden;
}
.components__project_card__content {
.content {
display: flex;
flex-direction: column;
}
.components__project_card__state {
.state {
padding: var(--_gap-half);
font-weight: bold;
}
.components__project_card__tags {
.tags {
display: flex;
flex-wrap: wrap;
gap: var(--_gap-half);
@ -60,7 +60,7 @@
}
}
.components__project_card__summary {
.summary {
text-wrap: balance;
flex-grow: 1;
}

View file

@ -1,4 +1,5 @@
import { JSX } from 'preact'
import { getStyleScope, useSmartStylesheet } from ':plugins/SmartStylesheet.tsx'
type ProjectCardProps = {
id: string
@ -9,25 +10,29 @@ type ProjectCardProps = {
state: 'complete' | 'progress' | 'stale' | 'pending'
}
const scope = getStyleScope(ProjectCard)
export function ProjectCard(
{ id, icon, title, summary, tags, state }: ProjectCardProps,
) {
useSmartStylesheet(import.meta, { scope })
return (
<div class='components__project_card'>
<div class={scope}>
<img
alt='Icon du projet'
src={icon}
class='components__project_card__icon'
class='icon'
/>
<div class='components__project_card__content'>
<div class='content'>
<h3>{title}</h3>
<div class='components__project_card__state'>
<div class='state'>
{toStateSpan(state)}
</div>
<div class='components__project_card__tags'>
<div class='tags'>
{tags.map((tag, index) => <span key={index}>{tag}</span>)}
</div>
<div class='components__project_card__summary'>
<div class='summary'>
{`${summary.slice(0, 150)} ...`}
</div>
<a href={`/projets/${id}`}>

View file

@ -1,4 +1,4 @@
.components__sponsor_cards {
:scope {
display: flex;
gap: var(--_gap);
flex-wrap: wrap;
@ -6,7 +6,7 @@
padding: var(--_gap);
}
.components__sponsor_card {
.card {
background-color: var(--_translucent);
aspect-ratio: 2/1;
padding: var(--_gap);

View file

@ -1,9 +1,14 @@
import { asset } from 'fresh/runtime'
import { Picture } from ':components/Picture.tsx'
import { getStyleScope, useSmartStylesheet } from ':plugins/SmartStylesheet.tsx'
const scope = getStyleScope(SponsorCards)
export function SponsorCards() {
useSmartStylesheet(import.meta, { scope })
return (
<div class='components__sponsor_cards'>
<div class={scope}>
{sponsors.map(SponsorCard)}
</div>
)
@ -13,7 +18,7 @@ function SponsorCard(
{ href, src, alt }: { href: string; src: string; alt: string },
) {
return (
<a class='components__sponsor_card' href={href} target='_blank'>
<a class='card' href={href} target='_blank'>
<Picture
src={asset(src)}
alt={alt}

View file

@ -35,6 +35,7 @@
":components/": "./components/",
":islands/": "./islands/",
":src/": "./src/",
":plugins/": "./plugins/",
":types": "./types.ts",
"@cohabit/mailer": "jsr:@cohabit/mailer@^0.3.3",
"@cohabit/resources-manager": "jsr:@cohabit/resources-manager@^0.2.1",

View file

@ -3,6 +3,7 @@ import { Markdown } from ':components/Markdown.tsx'
import { Signal, signal, useSignal } from '@preact/signals'
import { JSX } from 'preact'
import { useEffect, useRef } from 'preact/hooks'
import { useSmartStylesheet } from ':plugins/SmartStylesheet.tsx'
const systemHistory = signal<BotMessage[]>([{
role: 'system',
@ -66,6 +67,8 @@ async function aiRequest(
}
export default function AiChatBox() {
useSmartStylesheet(import.meta)
const dialog = useRef<HTMLDialogElement>(null)
const form = useRef<HTMLFormElement>(null)
const history = useSignal<JSX.Element[]>([])

View file

@ -1,4 +1,4 @@
.island__is_online {
:scope {
position: fixed;
bottom: 0;
width: 100%;

View file

@ -1,5 +1,6 @@
import { type Signal, useComputed, useSignal } from '@preact/signals'
import { useEffect } from 'preact/hooks'
import { getStyleScope, useSmartStylesheet } from ':plugins/SmartStylesheet.tsx'
type NetworkConnection = {
addEventListener: (
@ -14,18 +15,21 @@ declare global {
}
}
const scope = getStyleScope(IsOnline)
export default function IsOnline(
{ online, offline }: { online?: string; offline: string },
) {
useSmartStylesheet(import.meta, { scope })
const status = useSignal(true)
const displayed = useComputed(() => {
if (status.value && online) {
return <span>{online}</span>
return <span class={scope}>{online}</span>
}
if (status.value) {
return <span style={{ display: 'none' }}></span>
}
return <span class='island__is_online'>{offline}</span>
return <span class={scope}>{offline}</span>
})
useEffect(() => {

View file

@ -1,4 +1,4 @@
.island__login_form__form {
:scope {
padding: var(--_gap);
background-color: var(--_translucent);

View file

@ -7,14 +7,19 @@ import type {
} from '../routes/api/webauthn/login/[step].ts'
import { Button } from ':components/Button.tsx'
import { Input } from ':components/Input.tsx'
import { getStyleScope, useSmartStylesheet } from ':plugins/SmartStylesheet.tsx'
const scope = getStyleScope(LoginForm)
export default function LoginForm() {
useSmartStylesheet(import.meta, { scope })
return (
<form
onSubmit={connect}
method='POST'
action=''
className='island__login_form__form'
class={scope}
>
<Input
label='Email'

View file

@ -1,7 +1,10 @@
import { VNode } from 'preact'
import { useEffect, useRef } from 'preact/hooks'
import { useSmartStylesheet } from ':plugins/SmartStylesheet.tsx'
export default function MoreBox({ children }: { children: VNode | VNode[] }) {
useSmartStylesheet(import.meta)
const dialog = useRef<HTMLDialogElement>(null)
useEffect(() => {

View file

@ -1,6 +1,9 @@
import { useEffect, useRef } from 'preact/hooks'
import { useSmartStylesheet } from ':plugins/SmartStylesheet.tsx'
export default function SearchBox() {
useSmartStylesheet(import.meta)
const dialog = useRef<HTMLDialogElement>(null)
useEffect(() => {

View file

@ -1,4 +1,4 @@
.islands__theme_picker {
:scope {
display: inline-flex;
align-items: center;
gap: var(--_gap-half);

View file

@ -1,7 +1,12 @@
import { useComputed, useSignal, useSignalEffect } from '@preact/signals'
import { IS_BROWSER } from 'fresh/runtime'
import { getStyleScope, useSmartStylesheet } from ':plugins/SmartStylesheet.tsx'
const scope = getStyleScope(ThemePicker)
export default function ThemePicker() {
useSmartStylesheet(import.meta, { scope })
const colorSchemeQuery = '(prefers-color-scheme: dark)'
const colorSchemeDark = useSignal(
@ -39,7 +44,7 @@ export default function ThemePicker() {
})
return (
<label class='islands__theme_picker' title='Sélectionner un thème'>
<label class={scope} title='Sélectionner un thème'>
<span>{themeIcon}</span>
<select
value={theme}

View file

@ -1,6 +1,7 @@
import { App, fsRoutes, staticFiles } from 'fresh'
import { type State } from './utils.ts'
import { contentType } from 'jsr:@std/media-types@1/content-type'
import { smartStylesheetPlugin } from ':plugins/SmartStylesheet.tsx'
export const app = new App<State>()
@ -13,6 +14,8 @@ app.use((ctx) =>
app.use(staticFiles())
smartStylesheetPlugin(app)
//TEMP fix before updating cssBundler middleware
app.use(async (ctx) => {
const response = await ctx.next()

218
plugins/SmartStylesheet.tsx Normal file
View file

@ -0,0 +1,218 @@
import { asset, IS_BROWSER } from 'fresh/runtime'
import { App } from 'fresh'
/**
* List of css files imported by the current fresh route.
*/
const styles = new Map<
string,
{ url: string; layer: string | undefined; scope: string | undefined }
>()
const baseRoute = '__smart_css__'
/**
* Generate a css scope for the given component/island based on its name.
*
* @param component - Component or island to scope.
* @returns scope - css scope class.
*
* @example
* ```ts
* // ./(components|islands)/Button.tsx
* import type { JSX } from 'preact'
* import { getStyleScope, useSmartStylesheet } from './SmartStylesheet.tsx'
*
* const scope = getStyleScope(Button)
*
* export function Button(props: JSX.ButtonHTMLAttributes) {
* useSmartStylesheet(import.meta, { scope })
*
* return <button class={scope} {...props} />
* }
* ```
*/
export function getStyleScope<T extends { name: Readonly<string> }>(
component: T,
): string {
// generate scope/class hash
return `_scope_${
btoa(component.name).slice(-10).replaceAll('=', '_').toLowerCase()
}`
}
/**
* Hook to load component/island stylesheet only when
* it is imported by the served route.
* For any `./(component|islands)/Element.tsx` it will
* load the corresponding `./(component|islands)/Element.css`
*
* @param meta - Component ImportMeta used to resolve
* stylesheet path, name and layer.
* @param options - CSS scope to use (default: none)
* and css layer ('components' or 'islands') depending of the ImportMeta.
*
* @example Basic usage
* ```ts
* // ./(components|islands)/Button.tsx
* import type { JSX } from 'preact'
* import { useSmartStylesheet } from './SmartStylesheet.tsx'
*
* export function Button(props: JSX.ButtonHTMLAttributes) {
* useSmartStylesheet(import.meta)
*
* return <button class={scope} {...props} />
* }
* ```
*
* @example Use css scope
* ```ts
* // ./(components|islands)/Button.tsx
* import type { JSX } from 'preact'
* import { getStyleScope, useSmartStylesheet } from './SmartStylesheet.tsx'
*
* const scope = getStyleScope(Button)
*
* export function Button(props: JSX.ButtonHTMLAttributes) {
* useSmartStylesheet(import.meta, { scope })
*
* return <button class={scope} {...props} />
* }
* ```
*
* @example Use custom layer
* ```ts
* // ./(components|islands)/Button.tsx
* import type { JSX } from 'preact'
* import { getStyleScope, useSmartStylesheet } from './SmartStylesheet.tsx'
*
* const scope = getStyleScope(Button)
*
* export function Button(props: JSX.ButtonHTMLAttributes) {
* useSmartStylesheet(import.meta, { scope, layer: 'custom' })
*
* return <button class={scope} {...props} />
* }
* ```
*/
export function useSmartStylesheet(
meta: ImportMeta,
options?: { scope?: string; layer?: string },
) {
if (IS_BROWSER) return
// resolve filename
const css = meta.filename
?.replace('.tsx', '.css')
.replace(Deno.cwd(), '')
.replaceAll('\\', '/')
if (!css) return
if (styles.has(css)) return
// set css layer
const layerValue = options?.layer ?? css.includes('/components/')
? 'components'
: css.includes('/islands/')
? 'islands'
: undefined
styles.set(css, { url: css, scope: options?.scope, layer: layerValue })
}
/**
* Dynamic components and islands stylesheet.
*
* @param options - `pathname` set the pathname of the current route, `baseRoute` overrides default plugin baseRoute.
*
* @example
* ```ts
* import type { PageProps } from 'fresh'
* import { SmartStylesheet } from './SmartStylesheet.tsx'
*
* export default function App({ Component, url }: PageProps) {
* return (
* <html>
* <head>
* <meta charset='utf-8' />
* <meta name='viewport' content='width=device-width, initial-scale=1.0' />
* <title>My app</title>
* <link rel='stylesheet' href='/styles.css' />
* <SmartStylesheet pathname={url.pathname} />
* </head>
* <body>
* <Component />
* </body>
* </html>
* )
* }
* ```
*/
export function SmartStylesheet(
options: { pathname: string; baseRoute?: string },
) {
styles.clear()
return (
<link
href={asset(
`/${options.baseRoute ?? baseRoute}/__r__${
encodeURIComponent(options.pathname)
}`,
)}
rel='stylesheet'
/>
)
}
export function smartStylesheetPlugin<T>(
app: App<T>,
options: { baseRoute?: string } = {},
) {
options.baseRoute ??= baseRoute
//resolve dynamic styles imports
app.get(`/${options.baseRoute}/:path+`, async (ctx) => {
const { path } = ctx.params
if (path.startsWith('__r__')) {
const css = styles.values().map(({ url, layer, scope }) => {
const href = asset(`/${options.baseRoute}${url}`)
const scopeParam = scope ? `&__scope=${scope}` : ''
const cssImport = `@import url("${href}${scopeParam}")`
const cssLayer = layer ? ` layer(${layer})` : ''
return `${cssImport}${cssLayer};`
}).toArray()
return new Response(
`/* auto generated using jsr:@jotsr/smart-stylesheet */\n${
css.join('\n')
}`,
{
headers: {
'Content-Type': 'text/css; charset=utf-8',
},
},
)
}
if (path.startsWith('components') || path.startsWith('islands')) {
const scope = ctx.url.searchParams.get('__scope')
const css = await Deno.readTextFile(`./${path}`)
const file = scope ? `@scope (.${scope}) {\n\n${css}\n}` : css
return new Response(file, {
headers: {
'Content-Type': 'text/css; charset=utf-8',
},
})
}
return new Response(null, {
status: 400,
statusText: 'Bad Request - Invalid url',
})
})
}

View file

@ -4,9 +4,10 @@ import { Footer } from ':components/Footer.tsx'
import { Header } from ':components/Header.tsx'
import IsOnline from ':islands/IsOnline.tsx'
import RegisterServiceWorker from ':islands/RegisterServiceWorker.tsx'
import { SmartStylesheet } from ':plugins/SmartStylesheet.tsx'
export default function App(
{ Component, data }: PageProps<{ title?: string } | undefined>,
{ Component, data, url }: PageProps<{ title?: string } | undefined>,
) {
return (
<html lang='fr'>
@ -40,6 +41,7 @@ export default function App(
href={asset('/assets/favicon.ico')}
type='image/x-icon'
/>
<SmartStylesheet pathname={url.pathname} />
<link rel='stylesheet' href={asset('/main.css')} />
<link rel='stylesheet' href={asset('/imports/markdown_css')} />
<title>{data?.title ?? 'Fablab Coh@bit'}</title>

View file

@ -1,16 +0,0 @@
@import url('../../components/Header.css');
@import url('../../components/Footer.css');
@import url('../../components/Heros.css');
@import url('../../components/SponsorCards.css');
@import url('../../components/CohabitInfoTable.css');
@import url('../../components/BlogBlocks.css');
@import url('../../components/MachineCard.css');
@import url('../../components/ProjectCard.css');
@import url('../../components/AutoGrid.css');
@import url('../../components/MemberCard.css');
@import url('../../islands/ThemePicker.css');
@import url('../../islands/SearchBox.css');
@import url('../../islands/MoreBox.css');
@import url('../../islands/AiChatBox.css');
@import url('../../islands/LoginForm.css');
@import url('../../islands/IsOnline.css');

View file

@ -1,11 +1,10 @@
@import url('https://deno.land/x/univoq@0.2.0/stylesheets/reset.css')
layer(reset);
layer(reset);
@import url('https://unpkg.com/open-props') layer(framework);
@import url('https://cdn.jsdelivr.net/npm/remixicon@4.1.0/fonts/remixicon.css')
layer(framework);
layer(framework);
@import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@300..700&family=Hepta+Slab:wght@1..900&family=MuseoModerno:ital,wght@0,100..900;1,100..900&display=swap')
layer(framework);
layer(framework);
@import url('./base.css') layer(base);
@import url('./layout.css') layer(layout);
@import url('./components.css') layer(components);
@layer reset, framework, base, layout, components, utilities;
@layer reset, framework, base, layout, islands, components, utilities;