Composite Studio overview sections
This commit is contained in:
parent
edd88ecfc9
commit
b0fb22331a
5 changed files with 362 additions and 2 deletions
|
|
@ -1,5 +1,15 @@
|
|||
---
|
||||
title: Studio
|
||||
enableToc: false
|
||||
cssclasses:
|
||||
- gamecult-composite-overview
|
||||
compositeSections:
|
||||
- Open-Source-Model
|
||||
- Contributing
|
||||
- democratizing-gamedev
|
||||
- a-place-for-everyone
|
||||
- games-as-a-service
|
||||
- the-new-hotness
|
||||
---
|
||||
|
||||
# Studio
|
||||
|
|
@ -8,6 +18,8 @@ title: Studio
|
|||
|
||||
The studio section is where the older GameCult pitch turns into a durable shape: why the work is open, how people are meant to participate, what kind of projects fit here, and what the organization has historically promised to build around labor and governance.
|
||||
|
||||
The stitched studio narrative continues below. The standalone notes are still available from their own routes and in the sidebar.
|
||||
|
||||
## How We Work
|
||||
|
||||
- [Open Source Model](/Studio/Open-Source-Model)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { QuartzConfig } from "./quartz/cfg"
|
||||
import * as Plugin from "./quartz/plugins"
|
||||
import * as Component from "./quartz/components"
|
||||
|
||||
/**
|
||||
* Quartz 4 Configuration
|
||||
|
|
@ -86,8 +87,12 @@ const config: QuartzConfig = {
|
|||
emitters: [
|
||||
Plugin.AliasRedirects(),
|
||||
Plugin.ComponentResources(),
|
||||
Plugin.ContentPage(),
|
||||
Plugin.FolderPage(),
|
||||
Plugin.ContentPage({
|
||||
pageBody: Component.GameCultCompositeContent({ fallback: "content" }),
|
||||
}),
|
||||
Plugin.FolderPage({
|
||||
pageBody: Component.GameCultCompositeContent({ fallback: "folder" }),
|
||||
}),
|
||||
Plugin.ContentIndex({
|
||||
enableSiteMap: true,
|
||||
enableRSS: true,
|
||||
|
|
|
|||
210
quartz-site/quartz/components/GameCultCompositeContent.tsx
Normal file
210
quartz-site/quartz/components/GameCultCompositeContent.tsx
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
import { ComponentChildren } from "preact"
|
||||
import { Element, ElementContent, Root } from "hast"
|
||||
import { htmlToJsx } from "../util/jsx"
|
||||
import { QuartzPluginData } from "../plugins/vfile"
|
||||
import { clone } from "../util/clone"
|
||||
import { normalizeHastElement, resolveRelative, type FullSlug } from "../util/path"
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import FolderContent from "./pages/FolderContent"
|
||||
|
||||
interface Options {
|
||||
fallback: "content" | "folder"
|
||||
}
|
||||
|
||||
const defaultOptions: Options = {
|
||||
fallback: "content",
|
||||
}
|
||||
|
||||
type CompositeSection = {
|
||||
content: ComponentChildren
|
||||
leadFigure?: ComponentChildren
|
||||
sectionId: string
|
||||
slug: FullSlug
|
||||
title: string
|
||||
}
|
||||
|
||||
function isElement(node: ElementContent): node is Element {
|
||||
return node.type === "element"
|
||||
}
|
||||
|
||||
function folderSlug(slug: FullSlug) {
|
||||
return slug === "index" ? "" : slug.replace(/\/index$/, "")
|
||||
}
|
||||
|
||||
function resolveCompositeSlug(currentSlug: FullSlug, value: string) {
|
||||
const normalized = value
|
||||
.trim()
|
||||
.replace(/^\//, "")
|
||||
.replace(/\.md$/i, "")
|
||||
.replace(/\/$/, "")
|
||||
|
||||
if (normalized.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (normalized.includes("/")) {
|
||||
return normalized as FullSlug
|
||||
}
|
||||
|
||||
const folder = folderSlug(currentSlug)
|
||||
return (folder.length > 0 ? `${folder}/${normalized}` : normalized) as FullSlug
|
||||
}
|
||||
|
||||
function buildSectionContent(parentSlug: FullSlug, file: QuartzPluginData): CompositeSection | undefined {
|
||||
if (!file.slug || !file.htmlAst || !file.filePath) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const root = clone(file.htmlAst) as Root
|
||||
const children = [...root.children]
|
||||
|
||||
let firstMeaningfulIndex = -1
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const node = children[i]
|
||||
if (node.type === "text" && node.value.trim().length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
firstMeaningfulIndex = i
|
||||
break
|
||||
}
|
||||
|
||||
if (firstMeaningfulIndex >= 0) {
|
||||
const node = children[firstMeaningfulIndex]
|
||||
if (isElement(node) && node.tagName === "h1") {
|
||||
children.splice(firstMeaningfulIndex, 1)
|
||||
}
|
||||
}
|
||||
|
||||
let leadFigure: Element | undefined
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const node = children[i]
|
||||
if (node.type === "text" && node.value.trim().length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (isElement(node) && node.tagName === "figure") {
|
||||
leadFigure = node
|
||||
children.splice(i, 1)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
const normalizedChildren = children.map((child) =>
|
||||
normalizeHastElement(child as Element, parentSlug, file.slug as FullSlug),
|
||||
) as ElementContent[]
|
||||
|
||||
const sectionRoot: Root = {
|
||||
type: "root",
|
||||
children: normalizedChildren,
|
||||
}
|
||||
|
||||
const normalizedFigure = leadFigure
|
||||
? normalizeHastElement(leadFigure, parentSlug, file.slug as FullSlug)
|
||||
: undefined
|
||||
|
||||
const figureRoot: Root | undefined = normalizedFigure
|
||||
? {
|
||||
type: "root",
|
||||
children: [normalizedFigure],
|
||||
}
|
||||
: undefined
|
||||
|
||||
return {
|
||||
content: htmlToJsx(file.filePath, sectionRoot) as ComponentChildren,
|
||||
leadFigure: figureRoot ? (htmlToJsx(file.filePath, figureRoot) as ComponentChildren) : undefined,
|
||||
sectionId: file.slug.split("/").at(-1)?.toLowerCase() ?? "section",
|
||||
slug: file.slug as FullSlug,
|
||||
title: file.frontmatter?.title ?? file.slug.split("/").at(-1) ?? "Section",
|
||||
}
|
||||
}
|
||||
|
||||
function getCompositeSections(
|
||||
fileData: QuartzPluginData,
|
||||
allFiles: QuartzPluginData[],
|
||||
): CompositeSection[] {
|
||||
const currentSlug = fileData.slug as FullSlug | undefined
|
||||
const configured = fileData.frontmatter?.compositeSections
|
||||
|
||||
if (!currentSlug || !Array.isArray(configured) || configured.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const bySlug = new Map(
|
||||
allFiles
|
||||
.filter((file) => typeof file.slug === "string")
|
||||
.map((file) => [file.slug as FullSlug, file]),
|
||||
)
|
||||
|
||||
return configured
|
||||
.map((entry) => (typeof entry === "string" ? resolveCompositeSlug(currentSlug, entry) : undefined))
|
||||
.filter((entry): entry is FullSlug => entry !== undefined)
|
||||
.map((slug) => bySlug.get(slug))
|
||||
.filter((file): file is QuartzPluginData => file !== undefined)
|
||||
.map((file) => buildSectionContent(currentSlug, file))
|
||||
.filter((section): section is CompositeSection => section !== undefined)
|
||||
}
|
||||
|
||||
export default ((opts?: Partial<Options>) => {
|
||||
const options: Options = { ...defaultOptions, ...opts }
|
||||
const DefaultFolderContent = FolderContent()
|
||||
|
||||
const GameCultCompositeContent: QuartzComponent = (props: QuartzComponentProps) => {
|
||||
const { fileData, tree, allFiles } = props
|
||||
const baseContent = htmlToJsx(fileData.filePath!, tree) as ComponentChildren
|
||||
const classes: string[] = fileData.frontmatter?.cssclasses ?? []
|
||||
const articleClass = ["popover-hint", ...classes].join(" ")
|
||||
const sections = getCompositeSections(fileData, allFiles)
|
||||
|
||||
if (sections.length === 0) {
|
||||
if (options.fallback === "folder") {
|
||||
return <DefaultFolderContent {...props} />
|
||||
}
|
||||
|
||||
return <article class={articleClass}>{baseContent}</article>
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="popover-hint gamecult-composite-page">
|
||||
<article class={articleClass}>{baseContent}</article>
|
||||
<nav class="gamecult-composite-jump" aria-label={`${fileData.frontmatter?.title ?? "Page"} sections`}>
|
||||
{sections.map((section) => (
|
||||
<a href={`#${section.sectionId}`} class="gamecult-nav-chip">
|
||||
{section.title}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
<div class="gamecult-composite-sections">
|
||||
{sections.map((section) => (
|
||||
<section id={section.sectionId} class="gamecult-composite-section">
|
||||
<div class="gamecult-composite-divider" aria-hidden="true" />
|
||||
<div
|
||||
class={`gamecult-composite-shell${section.leadFigure ? " has-media" : ""}`}
|
||||
data-section-slug={section.slug}
|
||||
>
|
||||
{section.leadFigure && (
|
||||
<div class="gamecult-composite-media">
|
||||
{section.leadFigure}
|
||||
</div>
|
||||
)}
|
||||
<article class="gamecult-composite-copy">
|
||||
<header class="gamecult-composite-header">
|
||||
<h2>{section.title}</h2>
|
||||
<p class="gamecult-composite-standalone">
|
||||
<a href={resolveRelative(fileData.slug as FullSlug, section.slug)}>
|
||||
Open standalone note
|
||||
</a>
|
||||
</p>
|
||||
</header>
|
||||
<div class="gamecult-composite-body">{section.content}</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return GameCultCompositeContent
|
||||
}) satisfies QuartzComponentConstructor<Partial<Options>>
|
||||
|
|
@ -23,6 +23,7 @@ import Breadcrumbs from "./Breadcrumbs"
|
|||
import Comments from "./Comments"
|
||||
import Flex from "./Flex"
|
||||
import ConditionalRender from "./ConditionalRender"
|
||||
import GameCultCompositeContent from "./GameCultCompositeContent"
|
||||
|
||||
export {
|
||||
ArticleTitle,
|
||||
|
|
@ -50,4 +51,5 @@ export {
|
|||
Comments,
|
||||
Flex,
|
||||
ConditionalRender,
|
||||
GameCultCompositeContent,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -542,6 +542,123 @@ a:hover,
|
|||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.gamecult-composite-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.gamecult-composite-overview > h2,
|
||||
.gamecult-composite-overview > ul {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.gamecult-composite-jump {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.65rem;
|
||||
margin: 0;
|
||||
padding: 0 0.15rem;
|
||||
}
|
||||
|
||||
.gamecult-composite-sections {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.45rem;
|
||||
}
|
||||
|
||||
.gamecult-composite-section {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gamecult-composite-divider {
|
||||
height: 1.35rem;
|
||||
margin: 0 1.2rem -0.25rem;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(89, 183, 255, 0.1),
|
||||
rgba(255, 138, 42, 0.28),
|
||||
rgba(89, 183, 255, 0.08)
|
||||
);
|
||||
clip-path: polygon(0 24%, 100% 0, 100% 74%, 70% 100%, 0 82%);
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.gamecult-composite-shell {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 1rem;
|
||||
padding: 1.1rem;
|
||||
border-radius: 24px;
|
||||
border: 1px solid rgba(89, 183, 255, 0.14);
|
||||
background: linear-gradient(180deg, rgba(8, 13, 21, 0.8), rgba(8, 13, 21, 0.72));
|
||||
box-shadow:
|
||||
0 22px 44px rgba(0, 0, 0, 0.2),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.03);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.gamecult-composite-shell.has-media {
|
||||
grid-template-columns: minmax(280px, 0.88fr) minmax(0, 1.12fr);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.gamecult-composite-media {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.gamecult-composite-media > figure {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.gamecult-composite-media > figure img {
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
.gamecult-composite-copy {
|
||||
min-width: 0;
|
||||
padding: 0.15rem 0.1rem 0.2rem;
|
||||
}
|
||||
|
||||
.gamecult-composite-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 0.6rem 1.2rem;
|
||||
align-items: baseline;
|
||||
margin-bottom: 0.9rem;
|
||||
}
|
||||
|
||||
.gamecult-composite-header h2 {
|
||||
margin: 0;
|
||||
color: var(--dark);
|
||||
font-family: var(--titleFont);
|
||||
font-size: clamp(1.55rem, 2.4vw, 2.2rem);
|
||||
font-weight: 200;
|
||||
line-height: 0.96;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.gamecult-composite-standalone {
|
||||
margin: 0;
|
||||
font-family: var(--titleFont);
|
||||
font-size: 0.76rem;
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.gamecult-composite-body > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.gamecult-composite-body figure {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.gamecult-home-hero {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.15fr) minmax(280px, 0.85fr);
|
||||
|
|
@ -656,6 +773,20 @@ a:hover,
|
|||
}
|
||||
|
||||
@media all and ($mobile) {
|
||||
.gamecult-composite-shell,
|
||||
.gamecult-composite-shell.has-media {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
padding: 0.95rem;
|
||||
}
|
||||
|
||||
.gamecult-composite-divider {
|
||||
margin: 0 0.7rem -0.2rem;
|
||||
}
|
||||
|
||||
.gamecult-composite-header {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.gamecult-hero-panel {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue