Type to search pages and sections.

Skip to content
Ginko

Components

The @lupinum/ginko-components package with registry, Vue components and module. Every component with an example and its Markdown source.

What the package gives you

@lupinum/ginko-components provides building blocks for Markdown text: notices, tabs, cards, code groups, video, quizzes and more. The package has three parts:

  • The registry. This is pure data, without Vue. It says which tags exist, which props they have and where they may stand. Your ginko.config.ts loads it in Node.
  • The Vue components. They draw the tags. All classes start with ginko-. Colours come from the shadcn tokens of your site (see Theming).
  • The Nuxt module. It connects every tag to its component. <GinkoMarkdown> uses this mapping by itself.

This page shows every component live. Under each example is the Markdown text that produces it, and after it the list of props. The README of the package has the same lists as tables.

Set up

You need three steps.

Install the package

Terminal
npm install @lupinum/ginko @lupinum/ginko-i18n @lupinum/ginko-components

Add the module

The module goes into nuxt.config.ts:

ts
export default defineNuxtConfig({
  modules: ['@lupinum/ginko/nuxt', '@lupinum/ginko-components'],
})

With the @lupinum/ginko-docs layer you do not need the package or the second entry: the layer brings both, and components also comes from @lupinum/ginko-docs/config. The module needs @lupinum/ginko/nuxt for its texts; without it the build stops and says so.

Pass the registry to defineSite

ts
import { defineSite } from '@lupinum/ginko'
import { components } from '@lupinum/ginko-components/registry'

export default defineSite({
  // …
  components,
})

With components, Ginko checks the Markdown of your pages strictly. An unknown tag, an unknown prop or an unknown slot makes the build fail. A typo like ::callot shows up at build time and not for your readers. Without components, the parser refuses slots, and a component tag takes only title, lang, dir and aria-*: other props are refused, and the page leaves them out.

Write props

Props go in curly braces after the tag: ::card{title="Garden" iconColor="primary"}. Write each name exactly as in the lists on this page. Names with two words are camelCase (iconColor, defaultValue); icon-color is an unknown prop. A yes/no prop works on its own (open) or with "true" and "false".

Two props appear on many tags:

  • icon takes an Iconify name such as lucide:book-open; a bare name means Lucide. It renders with the <Icon> of @nuxt/icon, which the docs layer includes. Only icon sets your site installs work. Notices also know six built-in names (see callout).
  • appearance is quiet or tint (see Appearance).

Replace one component

<GinkoMarkdown> has a :components prop. A tag you give there wins over the mapping of the module, per tag. All other tags stay as they are.

vue
<script setup lang="ts">
import MyCallout from '~/components/MyCallout.vue'

const route = useRoute()
const doc = useGinkoEntry('docs', () => route.params.slug)
</script>

<template>
  <GinkoMarkdown :value="doc.data.body" :components="{ callout: MyCallout }" />
</template>

The Vue components of the package are exported from @lupinum/ginko-components/components. This way you can, for example, wrap Callout in your own variant instead of rewriting it.

Add your own component

Your site can have its own tags next to those of the package. Describe each one with defineComponents and add it to the registry. The strict check then knows its props, and the editor offers it.

ts
// ginko.config.ts
import { defineComponents, defineSite, f } from '@lupinum/ginko'
import { components } from '@lupinum/ginko-components/registry' // with the docs layer: from '@lupinum/ginko-docs/config'

const own = defineComponents({
  pricing: {
    kind: 'block',
    label: { de: 'Preis', en: 'Price' },
    describe: { de: 'Ein Preis mit Titel, darunter der Text.', en: 'A price with a title, the text below.' },
    props: {
      title: f.text({ de: 'Titel', en: 'Title' }, { max: 80 }),
      price: f.text({ de: 'Preis', en: 'Price' }, { max: 20 }),
    },
    editor: { example: '::pricing{title="Basic" price="9 €"}\nOne site, one language.\n::' },
  },
})

export default defineSite({
  // …
  components: { ...components, ...own },
})

A tag is lower case with hyphens; a prop is camelCase, may not start with on, and may not be as, innerHTML, outerHTML, innerText, outerText or textContent. Props can be text, longtext, number, boolean, choice, choices, image or ref. If your tag has the name of a tag of the package, your entry wins in the registry: the check and the editor use your props. The page still shows the component of the package, because the module's map wins over global components. To render your own Vue component for such a tag, pass it through <GinkoMarkdown :components> (see Replace one component). A global component works only for tag names the package does not use.

Then add the Vue component. Ginko looks for a global component with the name of the tag in PascalCase, so pricing becomes app/components/global/Pricing.vue. The props arrive as strings, the content as the default slot:

vue
<script setup lang="ts">
defineProps<{ title: string; price: string }>()
</script>

<template>
  <section class="pricing">
    <h3>{{ title }} <span>{{ price }}</span></h3>
    <slot />
  </section>
</template>

Change the UI texts

Fixed texts, for example “Copy code”, “Play” or the quiz buttons, live in the package's language files under the namespace components. They follow the language of the page. A site overrides one key in its own i18n/<code>.json:

json
{ "components": { "code": { "copy": "Copy" } } }

Copy a component into your app (shadcn-vue)

The npm package is the simple path: you get updates with the package. If you want to own and change the code of a component, copy it into your app with the shadcn-vue CLI. This site serves the registry:

Terminal
npx shadcn-vue@latest add https://ginko.lupinum.com/r/callout.json

The CLI writes the component and its helper files to components/ginko/ and installs the npm packages they need. Item names are the file names in kebab case (callout, code-block, timeline-item); /r/registry.json lists them all. Import the stylesheet once in your main CSS:

css
@import "./components/ginko/ginko-components.css";

A copied component has no Nuxt module behind it. Its texts are the English ones in components/ginko/texts.ts; change them there.

Appearance

Every framed component is quiet (a border) or tint (a filled area). Notices and excerpts are tint, everything else quiet. appearance="quiet" or appearance="tint" changes a single component; cards follow their group. In the HTML it becomes data-appearance.

Quiet
A border, no fill.
Tint
A fill, no border.

For the whole site, set two tokens in your CSS:

css
:root { --ginko-surface: var(--muted); --ginko-surface-border: transparent; }

Theming

The components read the shadcn tokens of your site: --background, --foreground, --card, --muted, --muted-foreground, --border, --primary, --primary-foreground, --ring, --radius and --font-mono. Each colour must be a full colour value (oklch(…), hsl(…) or hex). Old shadcn themes for Tailwind 3 store only numbers (--muted: 210 40% 96%); those give no colour here. Without the tokens the components use neutral colours that follow the colour mode of your site: dark with color-scheme: dark, the class .dark or data-theme="dark".

These names stay stable; everything else is internal:

  • Tokens: --ginko-surface and --ginko-surface-border (see above), --ginko-tint, the notice colours --ginko-info, --ginko-warning, --ginko-error, --ginko-success and --ginko-idea, and --ginko-scroll-margin (space above an API entry a link jumps to).
  • Code always sits on a dark panel: --ginko-code-frame, --ginko-code-bg, --ginko-code-fg, --ginko-code-muted and --ginko-code-hover.
  • The class ginko-<tag> on the outer element of each tag, for example ginko-callout or ginko-card, and the attribute data-appearance. Exceptions: the code block (pre) has ginko-code, a Markdown image ginko-img, and collapse without title has ginko-clip.

Notices

callout

Highlights a paragraph as a notice, with an icon and an optional title. type picks the kind: note, info (default), warning, error, success or idea.

Careful
This applies to new pages only.

Props: type, title, icon (one of file-text, info, triangle-alert, circle-x, circle-check, lightbulb, or an Iconify name), appearance.

note, info, warning, error, success, idea

The same boxes with a fixed type. You write the name of the kind instead of callout.

Note
A short thought on the side.
Good to know
The change applies at once.
Careful
You cannot undo this.
Not possible
The file is too large.
Done
The page is published.
Tip
A photo makes the page livelier.

Props: title, icon, appearance, as on callout.

Text

aside

A quiet remark next to the main text, with an optional heading.

Props: title (the heading), appearance.

excerpt

A highlighted quote or excerpt, with an optional source. size="display" makes the text large.

Whoever plants believes in tomorrow.
Anna Muster, 2024

Props: title, source, size (body default, display), appearance.

dropcap

The first letter of the paragraph is shown large. lines sets the height in lines (2 to 4).

Once upon a time there was a small garden at the edge of the town.

Props: lines (2 to 4, default 2).

kbd

Shows a key press as a key. This is an inline tag: you write it with one colon in the middle of a sentence.

Press Ctrl + S to save.

badge

A small coloured mark in the text, for example “new”. Also an inline tag. color is neutral, primary, success, warning or error.

The feature is new.

Props: color (neutral default, primary, success, warning, error).

read-more

A prominent link to another page, optionally with an icon (icon). Several in a row form one list.

To getting startedHow it all starts.
LanguagesPages in several languages.

Props: href (required), title (without it the address shows), icon, appearance.

inline-toc

Lists the headings of the page as links. It stays closed until someone opens it; open shows the list right away. Without headings it shows nothing. Here it lists the sections of this page:

On this page
md
::inline-toc{title="On this page" depth="2"}
::

Props: title, depth (2 to 4: down to which heading level), open, appearance.

Layout

flow

Keeps the text at a comfortable line length, with a background if you want (background: none, muted, tint).

A longer text that sits at reading width.

Props: width (reading default, wide), background (none default, muted, tint).

layout, column

Places columns side by side; on small screens they stack. layout is the frame, column is one column. width on the column sets its width.

Wide column
Narrow column

Props of layout: frame (none default, card, border, border-dashed, outline, outline-dashed), align (start, center, end), gap (none, sm, md default, lg), stack (sm, md default, lg: from which width the columns sit side by side), background (none default, muted, tint).

Props of column: width (xs to xl, default md), align (start, center, end), fit (natural default, cover, contain: how an image fills the column).

center

Centers the content in a narrow column, with a frame if you want (frame="card" and others).

See you soon!

Props: width (sm, md default, lg), frame (as on layout).

timeline, timeline-item

A vertical list of events in time order. active highlights one event, icon shows an icon instead of the dot.

Founding

2024
The club starts.

New garden

2026
The second plot is open.

timeline has no props. Props of timeline-item: date, meta, title, icon, active.

steps

Numbered steps; each heading in the block starts a step. mode="check" shows check marks instead of numbers.

Create an account

Open the start page.

Fill the page

Enter the texts.

Props: mode (numbered default, check), appearance.

cards, card

Places cards side by side (cols 1 to 3). A card has a title, text, and if you want an icon (icon, coloured with iconColor), an image and a link.

First card

Short text. Links inside work too: languages.

Second card

Another text.

Props of cards: cols (1 to 3, default 2), appearance.

Props of card: title, description, footer, href (the whole card becomes the link), icon, iconColor (primary, info, success, warning, error), image, imageWidth and imageHeight (its size in pixels; Ginko fills them from the file), horizontal (image beside the text), appearance.

Interactive

tabs, tab

Shows several contents as tabs, one of which is always visible. The first one is selected; icon puts an icon before the name. You can use the tabs with the keyboard. variant="line" underlines the selected tab and drops the frame; padded="false" lets a code block fill the tab. Without JavaScript only the first tab shows.

Readers find translated pages with search.
€12 a month.

Props of tabs: variant (separate default, line), padded (default on), appearance. Props of tab: title (required), icon.

accordion, accordion-item

A list of entries that open one by one. Opening one closes the others; multiple keeps several open. It uses <details> and works without JavaScript.

How do I sign in?
By email, with a code.
Does it cost anything?
No.

Props of accordion: multiple, appearance. Props of accordion-item: title (required), open.

collapse

With title, a section with a heading that readers open and close.

More details
The longer text is here.

Without title, long content (usually a code block) is cut off; “Show all … lines” opens it fully. The example is the ginko.config.ts of this site.

ginko.config.ts
// The Ginko docs. Every page comes from Markdown in content/docs; the layout,
// search and components come from @lupinum/ginko-docs. This site never asks the platform.
import { defineSite } from '@lupinum/ginko'
import { blogCollections, blogPages, changelogCollection, changelogPages, components, docsCollection, docsHome } from '@lupinum/ginko-docs/config'

const locales = ['de', 'en'] as const

export default defineSite({
  site: 'ginko-docs',
  name: 'Ginko',
  url: 'https://ginko.lupinum.com',
  timeZone: 'Europe/Vienna',
  locales,
  currency: 'EUR',
  pages: { home: { de: '/', en: '/en' }, blog: blogPages({ locales }), changelog: changelogPages({ locales }) },
  components,
  bricks: { home: docsHome(), docs: docsCollection({ locales }), ...blogCollections({ locales }), changelog: changelogCollection() },
})

Props: title, open (with title: open from the start), appearance.

Code

code-group

Shows several code blocks as tabs, named after file name or language.

npm install @lupinum/ginko

code-group has no props. Every code block in Markdown, also outside a group, shows its file name or language and a copy button.

code-tree

Shows a file tree and next to it the code of the selected file. Each code block needs the path as its file name.

<template><NuxtPage /></template>

Props: file (path of the file that is open first), expandAll (all folders open).

files

Shows files and folders as a list. A nested list becomes a folder, text after # a note at the end of the row. Known file types get their icon.

  • app
    • app.config.tsname and logo
    • app.vue
    • components
      • Header.vue
  • content
    • index.md
  • nuxt.config.ts
  • package.json

Props: active (path of the entry to mark), appearance.

Media

figure

An image with a caption. A click enlarges the image. alt is required. Plain Markdown images (![alt](src)) load lazily too.

The Ginko mark
The Ginko leaf

The Ginko mark

Props: src and alt (required), caption, width, height, bleed (wider than the text), aspect (auto default, square 1:1, landscape 4:3, portrait 3:4, video 16:9, wide 2:1), fit (cover default, contain), placement (inline default, start, end: float beside the text), frame (border default, none), zoom (default on).

A picture from the Files library brings its smaller copies, and the browser loads the smallest one that is sharp enough. When the figure crops to an aspect ratio, the picture's focus point stays in view: you set it in the library by clicking the image, and it applies wherever the picture is used. Enlarged, the figure always shows the whole picture. The older prop focus (top, bottom, left, right) still works in existing texts; the editor no longer offers it.

video

A YouTube, Vimeo or video link. Nothing is loaded from YouTube or Vimeo until the visitor clicks “Play”: no iframe, no thumbnail. Use poster to show your own image.

Props: src (required: a YouTube or Vimeo link, or an .mp4 or .webm file), title (required), poster, start (seconds).

embed

A map, a music player or a booking widget from a known provider. Like video, it loads nothing before the click: the visitor sees your poster or a neutral area, a button and one line that names the provider. An address that no provider knows becomes a plain link. Ginko never renders the HTML element embed or iframe from your Markdown: ::embed is always this component.

Props: src (required: the address the provider gives you to embed), title (required: the label of the button and the title of the frame), poster.

Providers

Built in:

IdAddressWhere you get it
google-mapshttps://www.google.com/maps/embed?pb=…Google Maps: Share → Embed a map
openstreetmaphttps://www.openstreetmap.org/export/embed.html?…openstreetmap.org: Share → HTML
spotify, spotify-listhttps://open.spotify.com/track/… (also episode, album, playlist, show)The link of the track or list
calendlyhttps://calendly.com/<user>/<event>The link of your booking page

You add a provider, replace one or turn one off in nuxt.config.ts:

ts
export default defineNuxtConfig({
  ginkoComponents: {
    embeds: {
      providers: {
        peertube: {
          name: 'PeerTube',
          pattern: '^https://peertube\\.example\\.org/w/([\\w-]+)$',
          embed: 'https://peertube.example.org/videos/embed/$1',
          allow: 'fullscreen',
        },
        spotify: false,
      },
    },
  },
})

name appears in the line before the click. pattern is a regular expression for the whole address and must start with ^https://. embed is the address of the frame, with $1 for the first group of the pattern, and must start with https://. ratio sets the shape (default 16/9), height a fixed height in pixels, allow the allow attribute of the frame. The first provider that matches wins. A wrong provider stops the build with a message that names it.

Only add providers you trust: every provider lets editors show that site in a frame on your pages.

Learning

quiz, quiz-question

Questions for self-checking. The questions appear one after another, the result comes at the end. No data is sent. The answers are a task list: - [x] is right, - [ ] is wrong. The explanation sits in the #explanation slot and appears only after checking.

Quick check

Two questions about the garden.

When is the best time to water?

Props of quiz: title, description, appearance. Props of quiz-question: question (required), mode (single default: one right answer, multiple: one or more).

API

api, api-param

Describes an HTTP interface: method and path, and below them the parameters and fields. Parameters with the same group sit together, one tab per group. The content of api-param is the description.

GET/v1/gardens
limit: number = 20since 1.2
Number of entries, at most 100.

Props of api: title, method (get, post, put, patch, delete, head, options), path, appearance. Props of api-param: name (required), group, type, required (shows a badge), deprecated, since, defaultValue.

Formulas and diagrams

Write formulas with $…$ in a text and with $$…$$ as a block of their own. They render with KaTeX, already in the HTML from the server: the area of a circle is A=πr2A = \pi r^2.

∫01x2 dx=13\int_0^1 x^2 \, dx = \frac{1}{3}

A code block with the language mermaid becomes a diagram. It is drawn in the browser:

KaTeX and Mermaid load only when a page needs them.