Jacky Zhao
2025-03-11 2718ab90194b3c7be600d1a2dadc4250207db1c0
feat: flex component, document higher-order layout components
2 files added
5 files modified
150 ■■■■■ changed files
docs/layout-components.md 62 ●●●●● patch | view | raw | blame | history
docs/layout.md 4 ●●● patch | view | raw | blame | history
quartz.layout.ts 11 ●●●● patch | view | raw | blame | history
quartz/components/DesktopOnly.tsx 8 ●●●● patch | view | raw | blame | history
quartz/components/Flex.tsx 55 ●●●●● patch | view | raw | blame | history
quartz/components/MobileOnly.tsx 8 ●●●● patch | view | raw | blame | history
quartz/components/index.ts 2 ●●●●● patch | view | raw | blame | history
docs/layout-components.md
New file
@@ -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())
```
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
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: [
quartz/components/DesktopOnly.tsx
@@ -1,7 +1,6 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
export default ((component?: QuartzComponent) => {
  if (component) {
export default ((component: QuartzComponent) => {
    const Component = component
    const DesktopOnly: QuartzComponent = (props: QuartzComponentProps) => {
      return <Component displayClass="desktop-only" {...props} />
@@ -12,7 +11,4 @@
    DesktopOnly.beforeDOMLoaded = component?.beforeDOMLoaded
    DesktopOnly.css = component?.css
    return DesktopOnly
  } else {
    return () => <></>
  }
}) satisfies QuartzComponentConstructor
}) satisfies QuartzComponentConstructor<QuartzComponent>
quartz/components/Flex.tsx
New file
@@ -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>
quartz/components/MobileOnly.tsx
@@ -1,7 +1,6 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
export default ((component?: QuartzComponent) => {
  if (component) {
export default ((component: QuartzComponent) => {
    const Component = component
    const MobileOnly: QuartzComponent = (props: QuartzComponentProps) => {
      return <Component displayClass="mobile-only" {...props} />
@@ -12,7 +11,4 @@
    MobileOnly.beforeDOMLoaded = component?.beforeDOMLoaded
    MobileOnly.css = component?.css
    return MobileOnly
  } else {
    return () => <></>
  }
}) satisfies QuartzComponentConstructor
}) satisfies QuartzComponentConstructor<QuartzComponent>
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,
}