From 2718ab90194b3c7be600d1a2dadc4250207db1c0 Mon Sep 17 00:00:00 2001
From: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Tue, 11 Mar 2025 21:56:43 +0000
Subject: [PATCH] feat: flex component, document higher-order layout components

---
 quartz/components/MobileOnly.tsx  |   26 ++---
 quartz/components/index.ts        |    2 
 quartz/components/Flex.tsx        |   55 +++++++++++++
 docs/layout.md                    |    4 
 docs/layout-components.md         |   62 +++++++++++++++
 quartz/components/DesktopOnly.tsx |   26 ++---
 quartz.layout.ts                  |   11 ++
 7 files changed, 153 insertions(+), 33 deletions(-)

diff --git a/docs/layout-components.md b/docs/layout-components.md
new file mode 100644
index 0000000..0c148a3
--- /dev/null
+++ b/docs/layout-components.md
@@ -0,0 +1,62 @@
+---
+title: Higher-Order Layout Components
+---
+
+Quartz provides several higher-order components that help with layout composition and responsive design. These components wrap other components to add additional functionality or modify their behavior.
+
+## `Flex` Component
+
+The `Flex` component creates a [flexible box layout](https://developer.mozilla.org/en-US/docs/Web/CSS/flex) that can arrange child components in various ways. It's particularly useful for creating responsive layouts and organizing components in rows or columns.
+
+```typescript
+type FlexConfig = {
+  components: {
+    Component: QuartzComponent
+    grow?: boolean // whether component should grow to fill space
+    shrink?: boolean // whether component should shrink if needed
+    basis?: string // initial main size of the component
+    order?: number // order in flex container
+    align?: "start" | "end" | "center" | "stretch" // cross-axis alignment
+    justify?: "start" | "end" | "center" | "between" | "around" // main-axis alignment
+  }[]
+  direction?: "row" | "row-reverse" | "column" | "column-reverse"
+  wrap?: "nowrap" | "wrap" | "wrap-reverse"
+  gap?: string
+}
+```
+
+### Example Usage
+
+```typescript
+Component.Flex({
+  components: [
+    {
+      Component: Component.Search(),
+      grow: true, // Search will grow to fill available space
+    },
+    { Component: Component.Darkmode() }, // Darkmode keeps its natural size
+  ],
+  direction: "row",
+  gap: "1rem",
+})
+```
+
+## `MobileOnly` Component
+
+The `MobileOnly` component is a wrapper that makes its child component only visible on mobile devices. This is useful for creating responsive layouts where certain components should only appear on smaller screens.
+
+### Example Usage
+
+```typescript
+Component.MobileOnly(Component.Spacer())
+```
+
+## `DesktopOnly` Component
+
+The `DesktopOnly` component is the counterpart to `MobileOnly`. It makes its child component only visible on desktop devices. This helps create responsive layouts where certain components should only appear on larger screens.
+
+### Example Usage
+
+```typescript
+Component.DesktopOnly(Component.TableOfContents())
+```
diff --git a/docs/layout.md b/docs/layout.md
index d8427c4..3f73753 100644
--- a/docs/layout.md
+++ b/docs/layout.md
@@ -35,7 +35,9 @@
 
 Quartz **components**, like plugins, can take in additional properties as configuration options. If you're familiar with React terminology, you can think of them as Higher-order Components.
 
-See [a list of all the components](component.md) for all available components along with their configuration options. You can also checkout the guide on [[creating components]] if you're interested in further customizing the behaviour of Quartz.
+See [a list of all the components](component.md) for all available components along with their configuration options. Additionally, Quartz provides several built-in higher-order components for layout composition - see [[layout-components]] for more details.
+
+You can also checkout the guide on [[creating components]] if you're interested in further customizing the behaviour of Quartz.
 
 ### Layout breakpoints
 
diff --git a/quartz.layout.ts b/quartz.layout.ts
index f45da0c..3a39d0b 100644
--- a/quartz.layout.ts
+++ b/quartz.layout.ts
@@ -25,8 +25,15 @@
   left: [
     Component.PageTitle(),
     Component.MobileOnly(Component.Spacer()),
-    Component.Search(),
-    Component.Darkmode(),
+    Component.Flex({
+      components: [
+        {
+          Component: Component.Search(),
+          grow: true,
+        },
+        { Component: Component.Darkmode() },
+      ],
+    }),
     Component.Explorer(),
   ],
   right: [
diff --git a/quartz/components/DesktopOnly.tsx b/quartz/components/DesktopOnly.tsx
index fe2a27f..ee80137 100644
--- a/quartz/components/DesktopOnly.tsx
+++ b/quartz/components/DesktopOnly.tsx
@@ -1,18 +1,14 @@
 import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
 
-export default ((component?: QuartzComponent) => {
-  if (component) {
-    const Component = component
-    const DesktopOnly: QuartzComponent = (props: QuartzComponentProps) => {
-      return <Component displayClass="desktop-only" {...props} />
-    }
-
-    DesktopOnly.displayName = component.displayName
-    DesktopOnly.afterDOMLoaded = component?.afterDOMLoaded
-    DesktopOnly.beforeDOMLoaded = component?.beforeDOMLoaded
-    DesktopOnly.css = component?.css
-    return DesktopOnly
-  } else {
-    return () => <></>
+export default ((component: QuartzComponent) => {
+  const Component = component
+  const DesktopOnly: QuartzComponent = (props: QuartzComponentProps) => {
+    return <Component displayClass="desktop-only" {...props} />
   }
-}) satisfies QuartzComponentConstructor
+
+  DesktopOnly.displayName = component.displayName
+  DesktopOnly.afterDOMLoaded = component?.afterDOMLoaded
+  DesktopOnly.beforeDOMLoaded = component?.beforeDOMLoaded
+  DesktopOnly.css = component?.css
+  return DesktopOnly
+}) satisfies QuartzComponentConstructor<QuartzComponent>
diff --git a/quartz/components/Flex.tsx b/quartz/components/Flex.tsx
new file mode 100644
index 0000000..1cf151e
--- /dev/null
+++ b/quartz/components/Flex.tsx
@@ -0,0 +1,55 @@
+import { concatenateResources } from "../util/resources"
+import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
+
+type FlexConfig = {
+  components: {
+    Component: QuartzComponent
+    grow?: boolean
+    shrink?: boolean
+    basis?: string
+    order?: number
+    align?: "start" | "end" | "center" | "stretch"
+    justify?: "start" | "end" | "center" | "between" | "around"
+  }[]
+  direction?: "row" | "row-reverse" | "column" | "column-reverse"
+  wrap?: "nowrap" | "wrap" | "wrap-reverse"
+  gap?: string
+}
+
+export default ((config: FlexConfig) => {
+  const Flex: QuartzComponent = (props: QuartzComponentProps) => {
+    const direction = config.direction ?? "row"
+    const wrap = config.wrap ?? "nowrap"
+    const gap = config.gap ?? "1rem"
+
+    return (
+      <div style={`display: flex; flex-direction: ${direction}; flex-wrap: ${wrap}; gap: ${gap};`}>
+        {config.components.map((c) => {
+          const grow = c.grow ? 1 : 0
+          const shrink = (c.shrink ?? true) ? 1 : 0
+          const basis = c.basis ?? "auto"
+          const order = c.order ?? 0
+          const align = c.align ?? "center"
+          const justify = c.justify ?? "center"
+
+          return (
+            <div
+              style={`flex-grow: ${grow}; flex-shrink: ${shrink}; flex-basis: ${basis}; order: ${order}; align-self: ${align}; justify-self: ${justify};`}
+            >
+              <c.Component {...props} />
+            </div>
+          )
+        })}
+      </div>
+    )
+  }
+
+  Flex.afterDOMLoaded = concatenateResources(
+    ...config.components.map((c) => c.Component.afterDOMLoaded),
+  )
+  Flex.beforeDOMLoaded = concatenateResources(
+    ...config.components.map((c) => c.Component.beforeDOMLoaded),
+  )
+  Flex.css = concatenateResources(...config.components.map((c) => c.Component.css))
+  return Flex
+}) satisfies QuartzComponentConstructor<FlexConfig>
diff --git a/quartz/components/MobileOnly.tsx b/quartz/components/MobileOnly.tsx
index 7d2108d..29958cf 100644
--- a/quartz/components/MobileOnly.tsx
+++ b/quartz/components/MobileOnly.tsx
@@ -1,18 +1,14 @@
 import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
 
-export default ((component?: QuartzComponent) => {
-  if (component) {
-    const Component = component
-    const MobileOnly: QuartzComponent = (props: QuartzComponentProps) => {
-      return <Component displayClass="mobile-only" {...props} />
-    }
-
-    MobileOnly.displayName = component.displayName
-    MobileOnly.afterDOMLoaded = component?.afterDOMLoaded
-    MobileOnly.beforeDOMLoaded = component?.beforeDOMLoaded
-    MobileOnly.css = component?.css
-    return MobileOnly
-  } else {
-    return () => <></>
+export default ((component: QuartzComponent) => {
+  const Component = component
+  const MobileOnly: QuartzComponent = (props: QuartzComponentProps) => {
+    return <Component displayClass="mobile-only" {...props} />
   }
-}) satisfies QuartzComponentConstructor
+
+  MobileOnly.displayName = component.displayName
+  MobileOnly.afterDOMLoaded = component?.afterDOMLoaded
+  MobileOnly.beforeDOMLoaded = component?.beforeDOMLoaded
+  MobileOnly.css = component?.css
+  return MobileOnly
+}) satisfies QuartzComponentConstructor<QuartzComponent>
diff --git a/quartz/components/index.ts b/quartz/components/index.ts
index 5b19794..49a3cb6 100644
--- a/quartz/components/index.ts
+++ b/quartz/components/index.ts
@@ -20,6 +20,7 @@
 import RecentNotes from "./RecentNotes"
 import Breadcrumbs from "./Breadcrumbs"
 import Comments from "./Comments"
+import Flex from "./Flex"
 
 export {
   ArticleTitle,
@@ -44,4 +45,5 @@
   NotFound,
   Breadcrumbs,
   Comments,
+  Flex,
 }

--
Gitblit v1.10.0