feat: rewrite components/islands css loading with dynamic tree-shake loader
This commit is contained in:
parent
e7bd691d53
commit
d461e53a00
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 où vous pouvez concrétiser vos
|
||||
projet, rencontrer des passionnés apprendre et développer votre savoir
|
||||
faire, dans l'entraide et le partage.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`}>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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[]>([])
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.island__is_online {
|
||||
:scope {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.island__login_form__form {
|
||||
:scope {
|
||||
padding: var(--_gap);
|
||||
background-color: var(--_translucent);
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.islands__theme_picker {
|
||||
:scope {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--_gap-half);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
3
main.ts
3
main.ts
|
|
@ -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
218
plugins/SmartStylesheet.tsx
Normal 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',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue