Fixes the copy on new sessions, some additional styling work
This commit is contained in:
@@ -39,7 +39,6 @@ migrate(
|
||||
continue documents;
|
||||
}
|
||||
}
|
||||
throw new Error(`Unrecognized data: ${JSON.stringify(data)}`);
|
||||
}
|
||||
},
|
||||
(app) => {
|
||||
|
||||
42
pb_migrations/1751155422_updated_relationships.js
Normal file
42
pb_migrations/1751155422_updated_relationships.js
Normal file
@@ -0,0 +1,42 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_617371094")
|
||||
|
||||
// update field
|
||||
collection.fields.addAt(0, new Field({
|
||||
"autogeneratePattern": "[a-z0-9]{15}",
|
||||
"hidden": false,
|
||||
"id": "text3208210256",
|
||||
"max": 15,
|
||||
"min": 15,
|
||||
"name": "id",
|
||||
"pattern": "^[a-z0-9]+$",
|
||||
"presentable": true,
|
||||
"primaryKey": true,
|
||||
"required": true,
|
||||
"system": true,
|
||||
"type": "text"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_617371094")
|
||||
|
||||
// update field
|
||||
collection.fields.addAt(0, new Field({
|
||||
"autogeneratePattern": "[a-z0-9]{15}",
|
||||
"hidden": false,
|
||||
"id": "text3208210256",
|
||||
"max": 15,
|
||||
"min": 15,
|
||||
"name": "id",
|
||||
"pattern": "^[a-z0-9]+$",
|
||||
"presentable": false,
|
||||
"primaryKey": true,
|
||||
"required": true,
|
||||
"system": true,
|
||||
"type": "text"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
34
pb_migrations/1751155435_updated_documents.js
Normal file
34
pb_migrations/1751155435_updated_documents.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_3332084752")
|
||||
|
||||
// update field
|
||||
collection.fields.addAt(2, new Field({
|
||||
"hidden": false,
|
||||
"id": "json2918445923",
|
||||
"maxSize": 0,
|
||||
"name": "data",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_3332084752")
|
||||
|
||||
// update field
|
||||
collection.fields.addAt(2, new Field({
|
||||
"hidden": false,
|
||||
"id": "json2918445923",
|
||||
"maxSize": 0,
|
||||
"name": "data",
|
||||
"presentable": true,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
40
pb_migrations/1751156191_updated_relationships.js
Normal file
40
pb_migrations/1751156191_updated_relationships.js
Normal file
@@ -0,0 +1,40 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_617371094")
|
||||
|
||||
// update field
|
||||
collection.fields.addAt(1, new Field({
|
||||
"cascadeDelete": true,
|
||||
"collectionId": "pbc_3332084752",
|
||||
"hidden": false,
|
||||
"id": "relation390457990",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "primary",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_617371094")
|
||||
|
||||
// update field
|
||||
collection.fields.addAt(1, new Field({
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "pbc_3332084752",
|
||||
"hidden": false,
|
||||
"id": "relation390457990",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "primary",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
@@ -63,7 +63,7 @@ export function AutoSaveTextarea({
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
className={`w-full min-h-[6em] field-sizing-content p-2 rounded border bg-slate-800 text-slate-100 border-slate-700 focus:outline-none focus:ring-2 focus:ring-violet-500 transition-colors ${flash ? "ring-2 ring-emerald-400 border-emerald-400 bg-emerald-950" : ""} ${className}`}
|
||||
className={`w-full min-h-[10em] field-sizing-content p-2 rounded border bg-slate-800 text-slate-100 border-slate-700 focus:outline-none focus:ring-2 focus:ring-violet-500 transition-colors ${flash ? "ring-2 ring-emerald-400 border-emerald-400 bg-emerald-950" : ""} ${className}`}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Document, DocumentId } from "@/lib/types";
|
||||
import type { AnyDocument, DocumentId } from "@/lib/types";
|
||||
import {
|
||||
Dialog,
|
||||
DialogPanel,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { Fragment, useCallback, useState } from "react";
|
||||
import * as Icons from "@/components/Icons.tsx";
|
||||
|
||||
type Props<T extends Document> = {
|
||||
type Props<T extends AnyDocument> = {
|
||||
title: React.ReactNode;
|
||||
error?: React.ReactNode;
|
||||
items: T[];
|
||||
@@ -26,7 +26,7 @@ type Props<T extends Document> = {
|
||||
* @param renderRow - Function to render each row's content
|
||||
* @param newItemForm - Function that renders a form for creating a new item; receives an onSubmit callback
|
||||
*/
|
||||
export function DocumentList<T extends Document>({
|
||||
export function DocumentList<T extends AnyDocument>({
|
||||
title,
|
||||
error,
|
||||
items,
|
||||
@@ -122,9 +122,6 @@ export function DocumentList<T extends Document>({
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<DialogPanel className="bg-slate-900 rounded-lg shadow-xl max-w-md w-full p-6 border border-slate-700 relative">
|
||||
<DialogTitle className="text-lg font-semibold text-slate-100 mb-4">
|
||||
Add New
|
||||
</DialogTitle>
|
||||
{newItemForm(handleFormSubmit)}
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -3,7 +3,7 @@ import { pb } from "@/lib/pocketbase";
|
||||
import { displayName } from "@/lib/relationships";
|
||||
import type {
|
||||
AnyDocument,
|
||||
Document,
|
||||
DocumentId,
|
||||
Relationship,
|
||||
RelationshipType,
|
||||
} from "@/lib/types";
|
||||
@@ -26,14 +26,15 @@ export function RelationshipList({
|
||||
root,
|
||||
relationshipType,
|
||||
}: RelationshipListProps) {
|
||||
const [items, setItems] = useState<Document[]>([]);
|
||||
const [items, setItems] = useState<AnyDocument[]>([]);
|
||||
const [relationshipId, setRelationshipId] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchItems() {
|
||||
const { items } = await queryClient.fetchQuery({
|
||||
const { relationship } = await queryClient.fetchQuery({
|
||||
queryKey: ["relationship", relationshipType, root.id],
|
||||
staleTime: 5 * 60 * 1000, // 5 mintues
|
||||
queryFn: async () => {
|
||||
@@ -49,36 +50,35 @@ export function RelationshipList({
|
||||
|
||||
setLoading(false);
|
||||
|
||||
return { items: relationship.expand?.secondary ?? [] };
|
||||
return { relationship };
|
||||
},
|
||||
});
|
||||
setItems(items);
|
||||
setRelationshipId(relationship.id);
|
||||
setItems(relationship.expand?.secondary ?? []);
|
||||
}
|
||||
|
||||
fetchItems();
|
||||
});
|
||||
}, [root, relationshipType]);
|
||||
|
||||
// Handles creation of a new document and adds it to the relationship
|
||||
const handleCreate = async (doc: Document) => {
|
||||
const handleCreate = async (doc: AnyDocument) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
// Check for existing relationship
|
||||
const existing = await pb.collection("relationships").getFullList({
|
||||
filter: `primary = "${root.id}" && type = "${relationshipType}"`,
|
||||
});
|
||||
if (existing.length > 0) {
|
||||
console.debug("Adding to existing relationship");
|
||||
await pb.collection("relationships").update(existing[0].id, {
|
||||
if (relationshipId) {
|
||||
console.debug("Adding to existing relationship", relationshipId);
|
||||
await pb.collection("relationships").update(relationshipId, {
|
||||
"+secondary": doc.id,
|
||||
});
|
||||
} else {
|
||||
console.debug("Creating new relationship");
|
||||
await pb.collection("relationships").create({
|
||||
const relationship = await pb.collection("relationships").create({
|
||||
primary: root.id,
|
||||
secondary: [doc.id],
|
||||
type: relationshipType,
|
||||
});
|
||||
setRelationshipId(relationship.id);
|
||||
}
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["relationship", relationshipType, root.id],
|
||||
@@ -91,6 +91,32 @@ export function RelationshipList({
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = async (documentId: DocumentId) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
if (relationshipId) {
|
||||
console.debug("Removing from existing relationship", relationshipId);
|
||||
await pb.collection("relationships").update(relationshipId, {
|
||||
secondary: items
|
||||
.map((item) => item.id)
|
||||
.filter((id) => id !== documentId),
|
||||
});
|
||||
}
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["relationship", relationshipType, root.id],
|
||||
});
|
||||
setItems((prev) => prev.filter((item) => item.id != documentId));
|
||||
} catch (e: any) {
|
||||
setError(
|
||||
e?.message || `Failed to remove document from ${relationshipType}.`,
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
<Loader />;
|
||||
}
|
||||
@@ -101,12 +127,12 @@ export function RelationshipList({
|
||||
items={items}
|
||||
error={error}
|
||||
renderRow={(document) => <DocumentRow document={document} />}
|
||||
removeItem={() => {}}
|
||||
removeItem={handleRemove}
|
||||
newItemForm={(onSubmit) => (
|
||||
<NewRelatedDocumentForm
|
||||
campaignId={root.campaign}
|
||||
relationshipType={relationshipType}
|
||||
onCreate={async (doc: Document) => {
|
||||
onCreate={async (doc: AnyDocument) => {
|
||||
await handleCreate(doc);
|
||||
onSubmit();
|
||||
}}
|
||||
|
||||
@@ -12,15 +12,15 @@ export type Props = {
|
||||
*/
|
||||
export const BasicRow = ({ doc, title, description }: Props) => {
|
||||
return (
|
||||
<li>
|
||||
<div>
|
||||
<Link
|
||||
to="/document/$documentId"
|
||||
params={{ documentId: doc.id }}
|
||||
className="text-lg"
|
||||
className="text-lg !no-underline text-slate-100 hover:underline hover:text-violet-400"
|
||||
>
|
||||
<h4>{title}</h4>
|
||||
</Link>
|
||||
{description && <p>{description}</p>}
|
||||
</li>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { RelationshipType, type CampaignId, type Document } from "@/lib/types";
|
||||
import {
|
||||
RelationshipType,
|
||||
type CampaignId,
|
||||
type AnyDocument,
|
||||
} from "@/lib/types";
|
||||
import { NewLocationForm } from "./location/NewLocationForm";
|
||||
import { NewMonsterForm } from "./monsters/NewMonsterForm";
|
||||
import { NewNpcForm } from "./npc/NewNpcForm";
|
||||
@@ -6,10 +10,6 @@ import { NewSceneForm } from "./scene/NewSceneForm";
|
||||
import { NewSecretForm } from "./secret/NewSecretForm";
|
||||
import { NewTreasureForm } from "./treasure/NewTreasureForm";
|
||||
|
||||
function assertUnreachable(_x: never): never {
|
||||
throw new Error("DocumentForm switch is not exhaustive");
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a form for any document type depending on the relationship.
|
||||
*/
|
||||
@@ -20,7 +20,7 @@ export const NewRelatedDocumentForm = ({
|
||||
}: {
|
||||
campaignId: CampaignId;
|
||||
relationshipType: RelationshipType;
|
||||
onCreate: (document: Document) => Promise<void>;
|
||||
onCreate: (doc: AnyDocument) => Promise<void>;
|
||||
}) => {
|
||||
switch (relationshipType) {
|
||||
case RelationshipType.Locations:
|
||||
@@ -38,6 +38,4 @@ export const NewRelatedDocumentForm = ({
|
||||
case RelationshipType.DiscoveredIn:
|
||||
return "Form not supported here";
|
||||
}
|
||||
|
||||
return assertUnreachable(relationshipType);
|
||||
};
|
||||
|
||||
@@ -5,9 +5,9 @@ import type { Location } from "@/lib/types";
|
||||
*/
|
||||
export const LocationPrintRow = ({ location }: { location: Location }) => {
|
||||
return (
|
||||
<li>
|
||||
<div>
|
||||
<h4>{location.data.name}</h4>
|
||||
<p>{location.data.description}</p>
|
||||
</li>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,5 +4,5 @@ import type { Monster } from "@/lib/types";
|
||||
* Renders an editable monster row
|
||||
*/
|
||||
export const MonsterPrintRow = ({ monster }: { monster: Monster }) => {
|
||||
return <li>{monster.data..name}</li>;
|
||||
return <div>{monster.data.name}</div>;
|
||||
};
|
||||
|
||||
@@ -5,9 +5,9 @@ import type { Npc } from "@/lib/types";
|
||||
*/
|
||||
export const NpcPrintRow = ({ npc }: { npc: Npc }) => {
|
||||
return (
|
||||
<li className="">
|
||||
<div className="">
|
||||
<h4>{npc.data.name}</h4>
|
||||
<p>{npc.data.description}</p>
|
||||
</li>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { useState } from "react";
|
||||
import type { CampaignId, Scene } from "@/lib/types";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { BaseForm } from "@/components/form/BaseForm";
|
||||
import { MultiLineInput } from "@/components/form/MultiLineInput";
|
||||
|
||||
/**
|
||||
* Renders a form to add a new scene. Calls onCreate with the new scene document.
|
||||
@@ -41,28 +43,22 @@ export const NewSceneForm = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
className="flex flex-col items-left gap-2 mt-4"
|
||||
<BaseForm
|
||||
title="Create new scene"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<h3>Create new scene</h3>
|
||||
<input
|
||||
type="text"
|
||||
className="flex-1 px-3 py-2 rounded bg-slate-800 text-slate-100 border border-slate-700 focus:outline-none focus:ring-2 focus:ring-violet-500"
|
||||
placeholder="Add a new scene..."
|
||||
error={error}
|
||||
buttonText={adding ? "Adding..." : "Create"}
|
||||
content={
|
||||
<>
|
||||
<MultiLineInput
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onChange={(v) => setText(v)}
|
||||
disabled={adding}
|
||||
placeholder="Scene description..."
|
||||
aria-label="Add new scene"
|
||||
/>
|
||||
{error && <div className="text-red-400 mt-2 text-sm">{error}</div>}
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 rounded bg-emerald-600 hover:bg-emerald-700 text-white font-semibold transition-colors disabled:opacity-60"
|
||||
disabled={adding || !text.trim()}
|
||||
>
|
||||
{adding ? "Adding..." : "Create"}
|
||||
</button>
|
||||
</form>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,5 +4,5 @@ import type { Scene } from "@/lib/types";
|
||||
* Renders an editable scene row
|
||||
*/
|
||||
export const ScenePrintRow = ({ scene }: { scene: Scene }) => {
|
||||
return <li className="">{scene.data.text}</li>;
|
||||
return <div className="">{scene.data.text}</div>;
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { Treasure } from "@/lib/types";
|
||||
*/
|
||||
export const TreasurePrintRow = ({ treasure }: { treasure: Treasure }) => {
|
||||
return (
|
||||
<li className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="flex-none accent-emerald-500 w-5 h-5"
|
||||
@@ -19,6 +19,6 @@ export const TreasurePrintRow = ({ treasure }: { treasure: Treasure }) => {
|
||||
<span className="italic text-slate-400">(No treasure text)</span>
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
32
src/components/form/BaseForm.tsx
Normal file
32
src/components/form/BaseForm.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
export type Props = {
|
||||
title?: string;
|
||||
content: React.ReactNode;
|
||||
buttonText?: string;
|
||||
isLoading?: boolean;
|
||||
error?: string | null;
|
||||
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||
};
|
||||
|
||||
export const BaseForm = ({
|
||||
title,
|
||||
content,
|
||||
buttonText,
|
||||
isLoading,
|
||||
error,
|
||||
onSubmit,
|
||||
}: Props) => {
|
||||
return (
|
||||
<form className="flex flex-col items-left gap-2" onSubmit={onSubmit}>
|
||||
<h3 className="text-lg font-semibold text-slate-100 mb-4">{title}</h3>
|
||||
<div>{content}</div>
|
||||
{error && <div className="text-red-400 mt-2 text-sm">{error}</div>}
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 rounded bg-emerald-600 hover:bg-emerald-700 text-white font-semibold transition-colors disabled:opacity-60"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{buttonText ? buttonText : "Submit"}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
22
src/components/form/MultiLineInput.tsx
Normal file
22
src/components/form/MultiLineInput.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
export type Props = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
className?: string;
|
||||
} & Omit<
|
||||
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
|
||||
"value" | "onChange" | "className"
|
||||
>;
|
||||
|
||||
export const MultiLineInput = ({
|
||||
value,
|
||||
onChange,
|
||||
className = "",
|
||||
...props
|
||||
}: Props) => (
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className={`w-full min-h-[10em] field-sizing-content p-2 rounded border bg-slate-800 text-slate-100 border-slate-700 focus:outline-none focus:ring-2 focus:ring-violet-500 transition-colors ${className}`}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -42,12 +42,9 @@ function RouteComponent() {
|
||||
// Check for a previous session
|
||||
const prevSession = await pb
|
||||
.collection("documents")
|
||||
.getFirstListItem(
|
||||
`campaign = "${campaign.id}" && json_extract(data, '$.session') != null`,
|
||||
{
|
||||
.getFirstListItem(`campaign = "${campaign.id}" && type = 'session'`, {
|
||||
sort: "-created",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
console.log("Previous session: ", {
|
||||
id: prevSession.id,
|
||||
@@ -62,9 +59,7 @@ function RouteComponent() {
|
||||
},
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ["campaign"] });
|
||||
|
||||
// If any, then copy things over
|
||||
// If any relations, then copy things over
|
||||
if (prevSession) {
|
||||
const prevRelations = await pb
|
||||
.collection<Relationship>("relationships")
|
||||
@@ -81,7 +76,7 @@ function RouteComponent() {
|
||||
await pb.collection("relationships").create({
|
||||
primary: newSession.id,
|
||||
type: relation.type,
|
||||
seciondary: relation.secondary,
|
||||
secondary: relation.secondary,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,14 +38,17 @@ function RouteComponent() {
|
||||
<TabGroup>
|
||||
<TabList className="flex flex-row flex-wrap gap-1 mt-2">
|
||||
{relationshipList.map((relationshipType) => (
|
||||
<Tab className="px-3 py-2 rounded bg-slate-800 text-slate-100 border border-slate-700 focus:outline-none focus:ring-2 focus:ring-violet-500 data-selected:bg-violet-900 data-selected:border-violet-700">
|
||||
<Tab
|
||||
key={relationshipType}
|
||||
className="px-3 py-2 rounded bg-slate-800 text-slate-100 border border-slate-700 focus:outline-none focus:ring-2 focus:ring-violet-500 data-selected:bg-violet-900 data-selected:border-violet-700"
|
||||
>
|
||||
{displayName(relationshipType)}
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
{relationshipList.map((relationshipType) => (
|
||||
<TabPanel>
|
||||
<TabPanel key={relationshipType}>
|
||||
<RelationshipList
|
||||
key={relationshipType}
|
||||
root={document}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
@import "tailwindcss";
|
||||
@tailwind utilities;
|
||||
|
||||
html, body {
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
background-color: #0f172a; /* slate-900 */
|
||||
color: #f1f5f9; /* slate-100 */
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -15,24 +17,15 @@ body {
|
||||
min-width: 320px;
|
||||
}
|
||||
|
||||
code, pre {
|
||||
font-family: 'Fira Mono', 'Menlo', 'Monaco', 'Consolas', monospace;
|
||||
code,
|
||||
pre {
|
||||
font-family: "Fira Mono", "Menlo", "Monaco", "Consolas", monospace;
|
||||
background: #1e293b; /* slate-800 */
|
||||
color: #a5b4fc; /* violet-300 */
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.2em 0.4em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #a5b4fc; /* violet-300 */
|
||||
text-decoration: underline;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
a:hover, a:focus {
|
||||
color: #7c3aed; /* violet-600 */
|
||||
}
|
||||
|
||||
/* Remove default outline, but keep focus-visible for accessibility */
|
||||
:focus:not(:focus-visible) {
|
||||
outline: none;
|
||||
|
||||
Reference in New Issue
Block a user