Frontend: CSS modules, Zustand + URL sync
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.panel-container {
|
||||
.panelContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 2rem;
|
||||
@@ -1,97 +1,64 @@
|
||||
import { useQueries } from "@tanstack/react-query";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { getArticleIds, getToc } from "./lib/api";
|
||||
import { Language } from "./lib/types";
|
||||
|
||||
import ArticleSelector from "./components/ArticleSelector";
|
||||
import Panel from "./components/Panel";
|
||||
import TOC from "./components/TOC";
|
||||
import ArticleSelector from "./components/ArticleSelector/ArticleSelector";
|
||||
import Panel from "./components/Panel/Panel";
|
||||
import TOC from "./components/TOC/TOC";
|
||||
|
||||
import useNavState from "./store/navStore";
|
||||
import useUIStore from "./store/uiStore";
|
||||
|
||||
import "./App.css";
|
||||
import styles from "./App.module.css";
|
||||
import CelexSelector from "./components/CelexSelector/CelexSelector";
|
||||
|
||||
type Props = {
|
||||
celexId: string;
|
||||
articleId: number;
|
||||
};
|
||||
|
||||
function App({ celexId, articleId }: Props) {
|
||||
const navigate = useNavigate();
|
||||
function App() {
|
||||
const { numPanels, addPanel } = useUIStore();
|
||||
const { celexId, articleId } = useNavState();
|
||||
|
||||
const results = useQueries({
|
||||
queries: [
|
||||
{
|
||||
queryKey: ["articleIds", celexId],
|
||||
queryFn: () => getArticleIds(celexId),
|
||||
queryFn: () => getArticleIds(celexId!),
|
||||
enabled: !!celexId,
|
||||
},
|
||||
{
|
||||
queryKey: ["toc", celexId],
|
||||
queryFn: () => getToc(celexId, Language.ENG),
|
||||
queryFn: () => getToc(celexId!, Language.ENG),
|
||||
enabled: !!celexId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const isPending = results.some((result) => result.isPending);
|
||||
const error = results.find((result) => result.isError);
|
||||
|
||||
if (isPending) {
|
||||
return <div className="panel">Loading...</div>;
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
if (error) {
|
||||
return <div className="panel">Error: {error.error?.message}</div>;
|
||||
return <div>Error: {error.error?.message}</div>;
|
||||
}
|
||||
|
||||
const examples = [
|
||||
{ name: "GDPR", id: "32016R0679" },
|
||||
{ name: "AI Act", id: "32024R1689" },
|
||||
{ name: "Cyber Resilience Act", id: "32024R2847" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<div className="controls">
|
||||
<div>
|
||||
<label htmlFor="examples">Select example:</label>
|
||||
<select
|
||||
id="examples"
|
||||
value={celexId}
|
||||
onChange={(e) => {
|
||||
navigate(`/${e.target.value}`);
|
||||
}}
|
||||
>
|
||||
{examples.map((example) => (
|
||||
<option key={example.id} value={example.id}>
|
||||
{example.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{/* <CelexSelector defaultId={celexId} onSelected={setCelexId} /> */}
|
||||
<ArticleSelector
|
||||
articleIds={results[0].data!}
|
||||
selectedId={articleId}
|
||||
onSelected={(id) => navigate(`/${celexId}/articles/${id}`)}
|
||||
/>
|
||||
<div className={styles.App}>
|
||||
<div className={styles.controls}>
|
||||
<CelexSelector />
|
||||
<ArticleSelector articleIds={results[0].data!} />
|
||||
<button onClick={addPanel}>Add Panel</button>
|
||||
</div>
|
||||
<div className="panel-container">
|
||||
<TOC
|
||||
toc={results[1].data!}
|
||||
selectedArticleId={articleId}
|
||||
onArticleSelected={(id) => navigate(`/${celexId}/articles/${id}`)}
|
||||
/>
|
||||
<div className={styles.panelContainer}>
|
||||
<TOC toc={results[1].data!} />
|
||||
{Array.from({ length: numPanels }, (_, index) => (
|
||||
<Panel
|
||||
key={index}
|
||||
celexId={celexId}
|
||||
celexId={celexId!}
|
||||
language={
|
||||
Object.values(Language)[index % Object.values(Language).length]
|
||||
}
|
||||
articleId={articleId}
|
||||
articleId={articleId!}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import App from "./App";
|
||||
|
||||
function MainView() {
|
||||
const { celexId, articleId } = useParams();
|
||||
if (!celexId) {
|
||||
return <div>Error: No CELEX ID provided</div>;
|
||||
}
|
||||
return (
|
||||
<App celexId={celexId} articleId={articleId ? parseInt(articleId) : 1} />
|
||||
);
|
||||
}
|
||||
export default MainView;
|
||||
@@ -1,37 +0,0 @@
|
||||
type ArticleSelectorProps = {
|
||||
articleIds: number[];
|
||||
selectedId: number;
|
||||
onSelected(articleId: number): void;
|
||||
};
|
||||
|
||||
function ArticleSelector({
|
||||
articleIds,
|
||||
selectedId,
|
||||
onSelected,
|
||||
}: ArticleSelectorProps) {
|
||||
return (
|
||||
<>
|
||||
{selectedId > 1 && (
|
||||
<button onClick={() => onSelected(selectedId - 1)}>prev</button>
|
||||
)}
|
||||
<select
|
||||
value={selectedId}
|
||||
onChange={(e) => {
|
||||
const id = parseInt(e.currentTarget.value);
|
||||
onSelected(id);
|
||||
}}
|
||||
>
|
||||
{articleIds.map((id) => (
|
||||
<option key={id} value={id}>
|
||||
Article {id}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{selectedId < articleIds[articleIds.length - 1] && (
|
||||
<button onClick={() => onSelected(selectedId + 1)}>next</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArticleSelector;
|
||||
34
frontend/src/components/ArticleSelector/ArticleSelector.tsx
Normal file
34
frontend/src/components/ArticleSelector/ArticleSelector.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import useNavState from "../../store/navStore";
|
||||
|
||||
type ArticleSelectorProps = {
|
||||
articleIds: number[];
|
||||
};
|
||||
|
||||
function ArticleSelector({ articleIds }: ArticleSelectorProps) {
|
||||
const { articleId, setArticleId } = useNavState();
|
||||
return (
|
||||
<>
|
||||
{articleId && articleId > 1 && (
|
||||
<button onClick={() => setArticleId(articleId - 1)}>prev</button>
|
||||
)}
|
||||
<select
|
||||
value={articleId || undefined}
|
||||
onChange={(e) => {
|
||||
const id = parseInt(e.currentTarget.value);
|
||||
setArticleId(id);
|
||||
}}
|
||||
>
|
||||
{articleIds.map((id) => (
|
||||
<option key={id} value={id}>
|
||||
Article {id}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{articleId && articleId < articleIds[articleIds.length - 1] && (
|
||||
<button onClick={() => setArticleId(articleId + 1)}>next</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArticleSelector;
|
||||
@@ -1,20 +0,0 @@
|
||||
type CelexSelectorProps = {
|
||||
defaultId?: string;
|
||||
onSelected(celexId: string): void;
|
||||
};
|
||||
|
||||
function CelexSelector({ defaultId, onSelected }: CelexSelectorProps) {
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
onSelected(e.currentTarget.celexId.value);
|
||||
}}
|
||||
>
|
||||
<label htmlFor="celexId">CELEX ID:</label>
|
||||
<input type="text" id="celexId" defaultValue={defaultId} />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default CelexSelector;
|
||||
33
frontend/src/components/CelexSelector/CelexSelector.tsx
Normal file
33
frontend/src/components/CelexSelector/CelexSelector.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import useNavState from "../../store/navStore";
|
||||
|
||||
const examples = [
|
||||
{ name: "GDPR", id: "32016R0679" },
|
||||
{ name: "AI Act", id: "32024R1689" },
|
||||
{ name: "Cyber Resilience Act", id: "32024R2847" },
|
||||
];
|
||||
|
||||
function CelexSelector() {
|
||||
const { celexId, setCelexId, setArticleId } = useNavState();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label htmlFor="examples">Select example:</label>
|
||||
<select
|
||||
id="examples"
|
||||
value={celexId || ""}
|
||||
onChange={(e) => {
|
||||
setCelexId(e.target.value);
|
||||
setArticleId(1);
|
||||
}}
|
||||
>
|
||||
{examples.map((example) => (
|
||||
<option key={example.id} value={example.id}>
|
||||
{example.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CelexSelector;
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Language } from "../lib/types";
|
||||
import "./LanguageSwitcher.css";
|
||||
|
||||
function LanguageSwitcher({
|
||||
defaultLang,
|
||||
onChange,
|
||||
}: {
|
||||
defaultLang: Language;
|
||||
onChange: (lang: Language) => void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<select
|
||||
defaultValue={defaultLang}
|
||||
onChange={(ev) => onChange(ev.currentTarget.value as Language)}
|
||||
className="language-switcher"
|
||||
>
|
||||
{Object.values(Language).map((lang) => (
|
||||
<option key={lang} value={lang}>
|
||||
{lang.toUpperCase()}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default LanguageSwitcher;
|
||||
@@ -1,4 +1,4 @@
|
||||
.language-switcher {
|
||||
.languageSwitcher {
|
||||
margin: 0 10px;
|
||||
padding: 5px;
|
||||
font-size: 14px;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Language } from "../../lib/types";
|
||||
import styles from "./LanguageSwitcher.module.css";
|
||||
|
||||
function LanguageSwitcher({
|
||||
defaultLang,
|
||||
onChange,
|
||||
}: {
|
||||
defaultLang: Language;
|
||||
onChange: (lang: Language) => void;
|
||||
}) {
|
||||
return (
|
||||
<select
|
||||
defaultValue={defaultLang}
|
||||
onChange={(ev) => onChange(ev.currentTarget.value as Language)}
|
||||
className={styles.languageSwitcher}
|
||||
>
|
||||
{Object.values(Language).map((lang) => (
|
||||
<option key={lang} value={lang}>
|
||||
{lang.toUpperCase()}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
export default LanguageSwitcher;
|
||||
@@ -1,25 +0,0 @@
|
||||
.panel {
|
||||
flex: 1 auto;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ccc;
|
||||
|
||||
.highlight {
|
||||
background-color: rgba(100, 255, 100, 0.2);
|
||||
}
|
||||
|
||||
article {
|
||||
.list-lower-alpha {
|
||||
list-style-type: lower-alpha;
|
||||
}
|
||||
|
||||
ol .item-number {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.paragraph-number {
|
||||
float: left;
|
||||
margin-right: 1ch;
|
||||
}
|
||||
}
|
||||
}
|
||||
6
frontend/src/components/Panel/Panel.module.css
Normal file
6
frontend/src/components/Panel/Panel.module.css
Normal file
@@ -0,0 +1,6 @@
|
||||
.panel {
|
||||
flex: 1 auto;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { getArticle } from "../lib/api";
|
||||
import { Language } from "../lib/types";
|
||||
import useUIStore from "../store/uiStore";
|
||||
import LanguageSwitcher from "./LanguageSwitcher";
|
||||
import "./Panel.css";
|
||||
|
||||
import { getArticle } from "../../lib/api";
|
||||
import { Language } from "../../lib/types";
|
||||
import useUIStore from "../../store/uiStore";
|
||||
import LanguageSwitcher from "../LanguageSwitcher/LanguageSwitcher";
|
||||
|
||||
import "../../styles/PanelContent.css";
|
||||
import styles from "./Panel.module.css";
|
||||
|
||||
type PanelProps = {
|
||||
celexId: string;
|
||||
@@ -20,7 +23,7 @@ function Panel({ celexId, language, articleId }: PanelProps) {
|
||||
const { data, isPending, error } = useQuery({
|
||||
queryKey: ["article", celexId, articleId, lang],
|
||||
queryFn: () => getArticle(celexId, articleId, lang),
|
||||
enabled: !!celexId,
|
||||
enabled: !!celexId && !!articleId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -73,7 +76,7 @@ function Panel({ celexId, language, articleId }: PanelProps) {
|
||||
if (error) return "An error has occurred: " + error.message;
|
||||
|
||||
return (
|
||||
<div className={"panel"}>
|
||||
<div className={styles.panel}>
|
||||
<LanguageSwitcher
|
||||
defaultLang={lang}
|
||||
onChange={setLang}
|
||||
@@ -81,7 +84,6 @@ function Panel({ celexId, language, articleId }: PanelProps) {
|
||||
<div
|
||||
ref={articleRef}
|
||||
lang={lang.substring(0, 2)}
|
||||
className="article-text"
|
||||
dangerouslySetInnerHTML={{ __html: data || "" }}
|
||||
/>
|
||||
</div>
|
||||
@@ -8,10 +8,11 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
overflow: scroll;
|
||||
max-height: 100vh;
|
||||
overflow-y: scroll;
|
||||
overflow-x: wrap;
|
||||
height: 100vh;
|
||||
|
||||
.toc-division {
|
||||
.tocDivision {
|
||||
margin-block: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -30,7 +31,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
.toggleButton {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
@@ -50,6 +51,6 @@
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.toggle-button:hover {
|
||||
.toggleButton:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
import { useState } from "react";
|
||||
import { Division } from "../lib/types";
|
||||
import "./TOC.css";
|
||||
import { Division } from "../../lib/types";
|
||||
import useNavState from "../../store/navStore";
|
||||
import styles from "./TOC.module.css";
|
||||
|
||||
type TOC = Division[];
|
||||
|
||||
type TOCProps = {
|
||||
toc: TOC;
|
||||
selectedArticleId?: number;
|
||||
onArticleSelected: (articleId: number) => void;
|
||||
};
|
||||
|
||||
function TOC({ toc, selectedArticleId, onArticleSelected }: TOCProps) {
|
||||
function TOC({ toc }: TOCProps) {
|
||||
const { articleId, setArticleId } = useNavState();
|
||||
|
||||
function containsArticle(division: Division, articleId: number): boolean {
|
||||
return division.content.some((c) => {
|
||||
if (c.type === "division") {
|
||||
@@ -19,20 +20,19 @@ function TOC({ toc, selectedArticleId, onArticleSelected }: TOCProps) {
|
||||
return c.type === "article" && c.id === articleId;
|
||||
});
|
||||
}
|
||||
function renderDivision(division: Division) {
|
||||
function renderDivision(div: Division) {
|
||||
return (
|
||||
<details
|
||||
key={division.title}
|
||||
className={`toc-division level-${division.level}`}
|
||||
open={
|
||||
!!selectedArticleId && containsArticle(division, selectedArticleId)
|
||||
}
|
||||
key={div.title}
|
||||
className={styles.tocDivision}
|
||||
data-division-level={div.level}
|
||||
open={!!articleId && containsArticle(div, articleId)}
|
||||
>
|
||||
<summary>
|
||||
{division.title}: {division.subtitle}
|
||||
{div.title}: {div.subtitle}
|
||||
</summary>
|
||||
<ul>
|
||||
{division.content.map((c) => {
|
||||
{div.content.map((c) => {
|
||||
if (c.type === "division") {
|
||||
return renderDivision(c);
|
||||
} else {
|
||||
@@ -40,10 +40,10 @@ function TOC({ toc, selectedArticleId, onArticleSelected }: TOCProps) {
|
||||
<li
|
||||
key={c.id}
|
||||
className={[
|
||||
"article",
|
||||
selectedArticleId === c.id ? "selected" : "",
|
||||
styles.article,
|
||||
articleId === c.id ? styles.selected : "",
|
||||
].join(" ")}
|
||||
onClick={() => onArticleSelected(c.id)}
|
||||
onClick={() => setArticleId(c.id)}
|
||||
>
|
||||
{c.title}: {c.subtitle}
|
||||
</li>
|
||||
@@ -57,10 +57,10 @@ function TOC({ toc, selectedArticleId, onArticleSelected }: TOCProps) {
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
|
||||
return (
|
||||
<nav className={`toc ${isVisible ? "" : "hidden"}`}>
|
||||
<nav className={[styles.toc, isVisible ? "" : styles.hidden].join(" ")}>
|
||||
<button
|
||||
onClick={() => setIsVisible(!isVisible)}
|
||||
className="toggle-button"
|
||||
className={styles.toggleButton}
|
||||
>
|
||||
{isVisible ? "<" : ">"}
|
||||
</button>
|
||||
38
frontend/src/hooks/urlSync.ts
Normal file
38
frontend/src/hooks/urlSync.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import useNavState from "../store/navStore";
|
||||
|
||||
export const useUrlSync = () => {
|
||||
const navigate = useNavigate();
|
||||
const { celexId: paramCelexId, articleId: paramArticleId } = useParams();
|
||||
const { articleId, celexId, setArticleId, setCelexId } = useNavState();
|
||||
|
||||
// Effect to read from URL when URL changes
|
||||
useEffect(() => {
|
||||
if (paramCelexId && paramCelexId !== celexId) {
|
||||
setCelexId(paramCelexId);
|
||||
}
|
||||
|
||||
if (paramArticleId) {
|
||||
const parsedArticleId = paramArticleId ? parseInt(paramArticleId) : null;
|
||||
if (parsedArticleId && parsedArticleId !== articleId) {
|
||||
setArticleId(parsedArticleId);
|
||||
}
|
||||
} else {
|
||||
// If no articleId in URL, reset Zustand state
|
||||
setArticleId(null);
|
||||
}
|
||||
}, [paramArticleId, paramArticleId]);
|
||||
|
||||
// Update the URL when Zustand changes
|
||||
useEffect(() => {
|
||||
if (celexId === null) {
|
||||
return;
|
||||
}
|
||||
let targetUrl = `/${celexId}`;
|
||||
if (articleId !== null) {
|
||||
targetUrl += `/articles/${articleId}`;
|
||||
}
|
||||
navigate(targetUrl, { replace: true });
|
||||
}, [navigate, celexId, articleId]); // Only sync URL when Zustand changes
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import TOC from "../components/TOC";
|
||||
import TOC from "../components/TOC/TOC";
|
||||
import { Language } from "./types";
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
@@ -3,9 +3,10 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||
import MainView from "./MainView";
|
||||
import MainView from "./pages/MainView";
|
||||
|
||||
import "./index.css";
|
||||
import LandingPage from "./pages/LandingPage";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
@@ -15,7 +16,7 @@ createRoot(document.getElementById("root")!).render(
|
||||
<ReactQueryDevtools />
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route index element={<div>Select a CELEX ID</div>} />
|
||||
<Route index element={<LandingPage />} />
|
||||
<Route path=":celexId/articles?/:articleId?">
|
||||
<Route index element={<MainView />} />
|
||||
</Route>
|
||||
|
||||
8
frontend/src/pages/LandingPage.tsx
Normal file
8
frontend/src/pages/LandingPage.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import CelexSelector from "../components/CelexSelector/CelexSelector";
|
||||
import { useUrlSync } from "../hooks/urlSync";
|
||||
|
||||
function LandingPage() {
|
||||
useUrlSync();
|
||||
return <CelexSelector />;
|
||||
}
|
||||
export default LandingPage;
|
||||
16
frontend/src/pages/MainView.tsx
Normal file
16
frontend/src/pages/MainView.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import App from "../App";
|
||||
import { useUrlSync } from "../hooks/urlSync";
|
||||
import useNavState from "../store/navStore";
|
||||
|
||||
function MainView() {
|
||||
useUrlSync();
|
||||
const celexId = useNavState((state) => state.celexId);
|
||||
|
||||
if (!celexId) {
|
||||
return <div>Error: No CELEX ID provided</div>;
|
||||
}
|
||||
|
||||
return <App />;
|
||||
}
|
||||
|
||||
export default MainView;
|
||||
17
frontend/src/store/navStore.ts
Normal file
17
frontend/src/store/navStore.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
interface NavState {
|
||||
celexId: string | null;
|
||||
articleId: number | null;
|
||||
setCelexId: (celexId: string) => void;
|
||||
setArticleId: (articleId: number | null) => void;
|
||||
}
|
||||
|
||||
const useNavState = create<NavState>((set) => ({
|
||||
celexId: null,
|
||||
articleId: null,
|
||||
setCelexId: (celexId) => set({ celexId }),
|
||||
setArticleId: (articleId) => set({ articleId }),
|
||||
}));
|
||||
|
||||
export default useNavState;
|
||||
20
frontend/src/styles/PanelContent.css
Normal file
20
frontend/src/styles/PanelContent.css
Normal file
@@ -0,0 +1,20 @@
|
||||
/* Styles for the embedded content inside the reading panel */
|
||||
|
||||
.highlight {
|
||||
background-color: rgba(100, 255, 100, 0.2);
|
||||
}
|
||||
|
||||
article {
|
||||
.list-lower-alpha {
|
||||
list-style-type: lower-alpha;
|
||||
}
|
||||
|
||||
ol .item-number {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.paragraph-number {
|
||||
float: left;
|
||||
margin-right: 1ch;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user