Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/genesis-docs-callout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@paper/genesis": minor
---

feat(genesis): add GnDocsCallout admonition primitive (#593)

A headless callout/admonition component for `tip`, `note`, `warning`, `caution`, and `important` types. Colors inherit from the active v0 theme via severity tokens (with standalone fallbacks); the `icon` and `title` slots override the inline-SVG and label defaults.
49 changes: 26 additions & 23 deletions apps/docs/src/components/docs/DocsCallout.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script setup lang="ts">
import { GnDocsCallout, type GnDocsCalloutType } from '@paper/genesis'

// Framework
import { useBreakpoints } from '@vuetify/v0'

Expand Down Expand Up @@ -65,6 +67,12 @@

const config = toRef(() => getCalloutConfig(props.type))

// askai / discord / tour are clickable action cards — docs-only, not
// admonitions. The five standard types delegate their shell to GnDocsCallout.
const interactive = toRef(() =>
props.type === 'askai' || props.type === 'discord' || props.type === 'tour',
)

const randomTip = shallowRef<CompiledTip>()
const mounted = shallowRef(false)

Expand Down Expand Up @@ -123,11 +131,11 @@

<template>
<div
v-if="!suppress"
v-if="interactive"
class="my-4 rounded-lg border-s-4 px-4 py-3"
:class="config.classes"
:role="props.type === 'askai' || props.type === 'discord' || props.type === 'tour' ? 'button' : undefined"
:tabindex="props.type === 'askai' || props.type === 'discord' || props.type === 'tour' ? 0 : undefined"
role="button"
:tabindex="0"
@click="onClick"
@keydown.enter="onClick"
@keydown.space.prevent="onClick"
Expand Down Expand Up @@ -156,7 +164,7 @@
</div>
</template>

<template v-else-if="props.type === 'tour'">
<template v-else>
<div class="flex items-center gap-2 mb-1">
<AppIcon :icon="config.icon" :size="18" />

Expand All @@ -170,15 +178,18 @@
{{ tour?.description ?? 'Click to start this interactive tour' }}
</div>
</template>
</div>

<template v-else-if="props.type === 'tip' && randomTip">
<div class="flex items-center gap-2 font-semibold mb-1">
<AppIcon :icon="config.icon" :size="18" />

<span>{{ config.title }}</span>
</div>
<GnDocsCallout
v-else-if="!suppress"
:type="(props.type as GnDocsCalloutType)"
>
<template #icon>
<AppIcon :icon="config.icon" :size="18" />
</template>

<div class="docs-alert-content text-on-surface" v-html="randomTip.bodyHtml" />
<template v-if="props.type === 'tip' && randomTip">
<div class="docs-alert-content" v-html="randomTip.bodyHtml" />

<RouterLink
v-if="randomTip.link"
Expand All @@ -189,18 +200,10 @@
</RouterLink>
</template>

<template v-else>
<div class="flex items-center gap-2 font-semibold mb-1">
<AppIcon :icon="config.icon" :size="18" />

<span>{{ config.title }}</span>
</div>

<div class="docs-alert-content text-on-surface">
<slot />
</div>
</template>
</div>
<div v-else class="docs-alert-content">
<slot />
</div>
</GnDocsCallout>
</template>

<style scoped>
Expand Down
21 changes: 20 additions & 1 deletion packages/genesis/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ packages/genesis/
│ ├── GnDocsExampleTabs/
│ ├── GnDocsExamplePanel/
│ ├── GnDocsExampleActions/
│ ├── GnDocsCallout/
│ └── GnPeek/
```

Expand Down Expand Up @@ -97,6 +98,24 @@ interface GnPeekProps {

`v-model:expanded` drives state. The default slot exposes `{ expanded }` for the label; a separate `icon` slot defaults to an inline chevron that rotates 180° when expanded. Both slots are overridable.

### `GnDocsCallout` — admonition shell

A presentational admonition box (info / tip / warning / caution / important). Pure shell — no interactivity, no app dependencies. Consumers layer behavior on top (the v0 docs site keeps the `askai` / `discord` / `tour` interactive callout types in its own `DocsCallout` wrapper, which delegates the five standard types to this component).

```ts
interface GnDocsCalloutProps {
type?: 'tip' | 'note' | 'warning' | 'caution' | 'important' // default: 'note'
}
```

`type` drives three things: the severity color, the default icon, and the default title. Color comes from a per-type v0 severity token consumed via the cascade with a standalone fallback — `tip → --v0-success`, `note → --v0-info`, `warning → --v0-warning`, `caution → --v0-error`, `important → --v0-accent`. These severity tokens are supplied by the consuming app (they are not part of v0's core token set); without them the hardcoded fallbacks render a reasonable standalone appearance.

| Slot | Exposes | Default |
|---|---|---|
| `icon` | `{ type }` | inline MDI SVG per type |
| `title` | `{ type }` | capitalized type name |
| default | — | callout body |

## Icon strategy

Action buttons expose icon slots with inline `<svg>` defaults using MDI paths.
Expand Down Expand Up @@ -155,7 +174,7 @@ Implementation sketch: `GnDocsExample` wraps its preview slot in `<div :data-the

In priority order:

1. `GnDocsCallout` (TIP / WARNING / ERROR / INFO admonitions)
1. ~~`GnDocsCallout` (TIP / WARNING / ERROR / INFO admonitions)~~ — shipped; see below.
2. `GnDocsCodeGroup` (tabbed code blocks)
3. `GnDocsKbd`, `GnDocsBadge`, `GnDocsCard` (atomic primitives)
4. `GnDocsApi` (API reference tables with prop / event / slot sections + hover popovers)
Expand Down
120 changes: 120 additions & 0 deletions packages/genesis/src/components/GnDocsCallout/GnDocsCallout.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<script lang="ts">
export type GnDocsCalloutType = 'tip' | 'note' | 'warning' | 'caution' | 'important'

export interface GnDocsCalloutProps {
/** Admonition severity — drives color token, default icon, and default title */
type?: GnDocsCalloutType
}

/** Default MDI path per type; overridable via the `icon` slot */
const ICONS: Record<GnDocsCalloutType, string> = {
tip: 'M20,11H23V13H20V11M1,11H4V13H1V11M13,1V4H11V1H13M4.92,3.5L7.05,5.64L5.63,7.05L3.5,4.93L4.92,3.5M16.95,5.63L19.07,3.5L20.5,4.93L18.37,7.05L16.95,5.63M12,6A6,6 0 0,1 18,12C18,14.22 16.79,16.16 15,17.2V19A1,1 0 0,1 14,20H10A1,1 0 0,1 9,19V17.2C7.21,16.16 6,14.22 6,12A6,6 0 0,1 12,6M14,21V22A1,1 0 0,1 13,23H11A1,1 0 0,1 10,22V21H14M11,18H13V15.87C14.73,15.43 16,13.86 16,12A4,4 0 0,0 12,8A4,4 0 0,0 8,12C8,13.86 9.27,15.43 11,15.87V18Z',
note: 'M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z',
warning: 'M13 14H11V9H13M13 18H11V16H13M1 21H23L12 2L1 21Z',
caution: 'M13 13H11V7H13M11 15H13V17H11M15.73 3H8.27L3 8.27V15.73L8.27 21H15.73L21 15.73V8.27L15.73 3Z',
important: 'M11,15H13V17H11V15M11,7H13V13H11V7M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20Z',
}

/** Default title per type; overridable via the `title` slot */
const TITLES: Record<GnDocsCalloutType, string> = {
tip: 'Tip',
note: 'Note',
warning: 'Warning',
caution: 'Caution',
important: 'Important',
}
</script>

<script setup lang="ts">
// Utilities
import { toRef } from 'vue'

defineOptions({ name: 'GnDocsCallout' })

const { type = 'note' } = defineProps<GnDocsCalloutProps>()

const path = toRef(() => ICONS[type])
const label = toRef(() => TITLES[type])
</script>

<template>
<div class="genesis-docs-callout" :data-type="type">
<div class="genesis-docs-callout__header">
<slot name="icon" :type>
<svg
aria-hidden="true"
class="genesis-docs-callout__icon"
fill="currentColor"
height="18"
viewBox="0 0 24 24"
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<path :d="path" />
</svg>
</slot>

<span class="genesis-docs-callout__title">
<slot name="title" :type>{{ label }}</slot>
</span>
</div>

<div class="genesis-docs-callout__body">
<slot />
</div>
</div>
</template>

<style scoped>
.genesis-docs-callout {
/* Internal alias, resolved from v0 severity tokens with standalone fallbacks. */
--gn-callout-color: var(--v0-info, #3b82f6);

margin-block: 1rem;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
border-inline-start: 4px solid color-mix(in srgb, var(--gn-callout-color) 50%, transparent);
background: color-mix(in srgb, var(--gn-callout-color) 10%, transparent);
}

.genesis-docs-callout[data-type='tip'] {
--gn-callout-color: var(--v0-success, #3fb950);
}

.genesis-docs-callout[data-type='warning'] {
--gn-callout-color: var(--v0-warning, #d29922);
}

.genesis-docs-callout[data-type='caution'] {
--gn-callout-color: var(--v0-error, #f85149);
}

.genesis-docs-callout[data-type='important'] {
--gn-callout-color: var(--v0-accent, #a371f7);
}

.genesis-docs-callout__header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.25rem;
font-weight: 600;
color: var(--gn-callout-color);
}

.genesis-docs-callout__icon {
flex: none;
}

.genesis-docs-callout__body {
color: var(--v0-on-surface, #1a1c1e);
}

.genesis-docs-callout__body :deep(> p:first-child) {
margin-top: 0;
}

.genesis-docs-callout__body :deep(> p:last-child) {
margin-bottom: 0;
}
</style>
2 changes: 2 additions & 0 deletions packages/genesis/src/components/GnDocsCallout/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type { GnDocsCalloutProps, GnDocsCalloutType } from './GnDocsCallout.vue'
export { default as GnDocsCallout } from './GnDocsCallout.vue'
1 change: 1 addition & 0 deletions packages/genesis/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './GnActionButton'
export * from './GnDocsCallout'
export * from './GnDocsExample'
export * from './GnDotGrid'
export * from './GnPeek'
Loading