Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a0a1ab4
feat(bulma): add @paper/bulma — Bulma compat design system (Tier 1)
johnleider Aug 1, 2026
14bc46b
fix(bulma): warn in dev when BuHelp validation has no ambient Input.Root
johnleider Aug 1, 2026
6c50c9f
refactor(bulma): express region slots as Bulma part components
johnleider Aug 4, 2026
d547014
docs(bulma): add Design Systems docs section with Bulma pilot
johnleider Aug 4, 2026
322a50b
chore(dev): alias @paper/bulma in the dev playground
johnleider Aug 4, 2026
ed0dae3
docs(bulma): expand BuDropdown page with live examples and behavior docs
johnleider Aug 4, 2026
bb75ba4
docs(bulma): float dropdown menus over the page via clipped frame pro…
johnleider Aug 4, 2026
e346d56
docs(bulma): single-owner frame promotion; hover examples grow, never…
johnleider Aug 4, 2026
c860431
docs(bulma): tier frame promotion — float slides under chrome, overla…
johnleider Aug 4, 2026
bca0ed3
docs(bulma): eliminate promotion flash — pin before measuring clip re…
johnleider Aug 4, 2026
576e49d
docs(bulma): classify frame promotion by DOM predicate, not event heu…
johnleider Aug 4, 2026
c9c091b
feat(bulma): add BuNumberField — first Tier 2 component
johnleider Aug 4, 2026
8476e84
docs(bulma): add BuNumberField page — Tier 2 docs precedent
johnleider Aug 4, 2026
e492621
docs(bulma): first-paint theme + stable frame heights in example sand…
johnleider Aug 6, 2026
baa7b0c
Merge origin/dev into feat/paper-bulma
johnleider Aug 20, 2026
cbf98d7
docs(bulma): complete component pages and systems example chrome
johnleider Aug 21, 2026
b61361e
chore(bulma): add changeset for first @paper/bulma publish
johnleider Aug 21, 2026
7ad8234
chore(bulma): start at 0.0.0 until first publish
johnleider Aug 21, 2026
0147556
chore(ci): keep Playwright projects out of the unit job
johnleider Aug 22, 2026
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
9 changes: 9 additions & 0 deletions .changeset/paper-bulma.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@paper/bulma": minor
---

feat(bulma): Vue behavior for Bulma's markup (#760)

`@paper/bulma` is a compat design system: components render the markup and classes [bulma.io](https://bulma.io/documentation/) documents, against the `bulma.css` you already load. There is no theme, no class prefix, and no CSS in the package. `bulma` is an optional peer.

What ships is the JavaScript Bulma never did — open state, click-outside, Escape, focus, and form wiring — for the documented component, element, and form families, plus `BuNumberField` composed from form addons.
6 changes: 4 additions & 2 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@ jobs:
# components had an SF: entry in neither report). The v8 provider only
# emits records for files a run actually loaded, so a wide include costs
# this job nothing — it does not backfill untouched files at 0%.
# `*:browser` (v0:browser, bulma:browser) needs Playwright Chromium —
# those projects run in the `browser` job, which installs the binary.
- name: Run unit tests with coverage
run: pnpm test:run --coverage --project '!v0:browser'
run: pnpm test:run --coverage --project '!*:browser'
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
Expand Down Expand Up @@ -90,7 +92,7 @@ jobs:
# PR #725 to duplicate its browser assertions as unit tests to recover
# codecov/patch (70.83% -> 100%). Keep the include wide.
- name: Run component tests with coverage
run: pnpm exec vitest run --project v0:browser --coverage
run: pnpm exec vitest run --project '*:browser' --coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,4 @@ apps/docs/build/generated/
# Vitest failure artifacts
packages/0/.vitest-attachments/
**/__screenshots__/
.vitest-attachments/
13 changes: 9 additions & 4 deletions apps/docs/build/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,18 +80,19 @@ export function applyMarkdownPlugins (md: MarkdownIt, highlighter: DocsHighlight
const hasOrders = orders?.some(o => o !== undefined)
const collapse = env._exampleCollapse
const collapseAttr = collapse ? ' collapse' : ''
const noResizeAttr = env._exampleNoResize ? ' disable-resize' : ''
const imports = env._exampleImports as Record<string, string>
const importsAttr = Object.keys(imports).length > 0
? ` :imports="${JSON.stringify(imports).replace(/"/g, '\'')}"`
: ''
const exampleTag = env._exampleTag as string

if (paths.length === 1) {
return `<${exampleTag} file-path="${paths[0]}"${collapseAttr}${importsAttr}>\n<template #description>\n`
return `<${exampleTag} file-path="${paths[0]}"${collapseAttr}${noResizeAttr}${importsAttr}>\n<template #description>\n`
}
const pathsJson = JSON.stringify(paths).replace(/"/g, '\'')
const ordersAttr = hasOrders ? ` :file-orders="${JSON.stringify(orders)}"` : ''
return `<${exampleTag} :file-paths="${pathsJson}"${ordersAttr}${collapseAttr}${importsAttr}>\n<template #description>\n`
return `<${exampleTag} :file-paths="${pathsJson}"${ordersAttr}${collapseAttr}${noResizeAttr}${importsAttr}>\n<template #description>\n`
}

function renderExampleClose (env: Record<string, unknown>, fallbackTag: string): string {
Expand All @@ -100,6 +101,7 @@ export function applyMarkdownPlugins (md: MarkdownIt, highlighter: DocsHighlight
const orders = env._exampleFileOrders as (number | undefined)[]
const hasOrders = orders?.some(o => o !== undefined)
const collapse = env._exampleCollapse
const noResize = env._exampleNoResize
const imports = env._exampleImports as Record<string, string>
const exampleTag = (env._exampleTag as string) || fallbackTag

Expand All @@ -111,21 +113,23 @@ export function applyMarkdownPlugins (md: MarkdownIt, highlighter: DocsHighlight
delete env._exampleOpened
delete env._examplePathPara
delete env._exampleCollapse
delete env._exampleNoResize

const collapseAttr = collapse ? ' collapse' : ''
const noResizeAttr = noResize ? ' disable-resize' : ''
const importsAttr = Object.keys(imports).length > 0
? ` :imports="${JSON.stringify(imports).replace(/"/g, '\'')}"`
: ''

if (wasOpened) return `</template>\n</${exampleTag}>\n`

if (paths?.length === 1) {
return `<${exampleTag} file-path="${paths[0]}"${collapse ? collapseAttr : ' peek'}${importsAttr} />\n`
return `<${exampleTag} file-path="${paths[0]}"${collapse ? collapseAttr : ' peek'}${noResizeAttr}${importsAttr} />\n`
}
if (paths?.length > 1) {
const pathsJson = JSON.stringify(paths).replace(/"/g, '\'')
const ordersAttr = hasOrders ? ` :file-orders="${JSON.stringify(orders)}"` : ''
return `<${exampleTag} :file-paths="${pathsJson}"${ordersAttr}${collapseAttr}${importsAttr} />\n`
return `<${exampleTag} :file-paths="${pathsJson}"${ordersAttr}${collapseAttr}${noResizeAttr}${importsAttr} />\n`
}

return ''
Expand All @@ -142,6 +146,7 @@ export function applyMarkdownPlugins (md: MarkdownIt, highlighter: DocsHighlight
env._exampleFileOrders = [] as (number | undefined)[]
env._exampleImports = {} as Record<string, string>
env._exampleCollapse = info.includes('collapse')
env._exampleNoResize = info.includes('no-resize')
return ''
}

Expand Down
66 changes: 66 additions & 0 deletions apps/docs/sandbox/bulma.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex">

<title>Bulma example</title>
<link rel="icon" type="image/png" href="/0.png">

<script>
// Blocking, and first: an iframe composites an opaque canvas as soon as
// it is attached, so anything that decides the scheme later — the CDN
// stylesheet, the app bundle — decides it after the reader has already
// seen a white frame. This runs before either exists.
document.documentElement.dataset.theme =
new URLSearchParams(location.search).get('theme') === 'dark' ? 'dark' : 'light'
</script>

<style>
/* Bulma's own `--bulma-scheme-main` and `--bulma-text`, resolved and
inlined so the document paints the right surface while the CDN request
is still in flight. Zero specificity via `:where()`, so Bulma takes
these back the moment it lands and stays the source of truth. */
:where(html[data-theme="dark"]) {
background-color: #14161a;
color: #abb1bf;
color-scheme: dark;
}

:where(html[data-theme="light"]) {
background-color: #fff;
color: #404654;
color-scheme: light;
}
</style>

<!--
The user's own bulma.css is the styling source — @paper/bulma ships none.
This document loads it (and Font Awesome, which Bulma's documented markup
uses for icons) exactly the way a zero-config consumer would, and loads
nothing from the docs shell: no UnoCSS preflight, no theme tokens.
-->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.5.2/css/all.min.css">

<style>
/* Bulma's reset reserves a permanent scrollbar track (`overflow-y: scroll`),
which reads as a dead scrollbar inside a frame that is sized to its own
content. */
html {
overflow-y: auto;
}

/* Nothing here forces transparency while promoted: an iframe paints an
opaque canvas whatever its document says, so the parent clips the frame
to the regions this example is actually painting instead. Leaving
Bulma's own background in place is what makes the clip edge blend —
made transparent, the cut showed the white canvas as a bright rim. */
</style>
</head>
<body>
<div id="sandbox"></div>
<script type="module" src="/src/sandbox/bulma.ts"></script>
</body>
</html>
2 changes: 1 addition & 1 deletion apps/docs/src/components/app/AppBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
<template>
<Atom
:as
:class="['flex items-center justify-between h-[48px] fixed inset-x-0 top-[var(--app-banner-h,24px)] px-3 text-on-surface border-b border-solid border-divider z-1', settings.surface.value]"
:class="['flex items-center justify-between h-[48px] fixed inset-x-0 top-[var(--app-banner-h,24px)] px-3 text-on-surface border-b border-solid border-divider z-1', settings.showBgGlass.value ? 'bg-glass-surface' : 'bg-surface']"
data-app-bar
>
<div class="flex items-center gap-2">
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/src/components/app/AppNav.vue
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@
aria-label="Main navigation"
as="nav"
:class="[
'flex flex-col fixed w-[230px] top-0 md:top-[calc(48px+var(--app-banner-h,24px))] bottom-0 start-0 ltr:-translate-x-full rtl:translate-x-full md:ltr:translate-x-0 md:rtl:translate-x-0 border-e border-solid border-divider',
'flex flex-col fixed w-[230px] top-0 md:top-[calc(48px+var(--app-banner-h,24px))] bottom-0 start-0 -translate-x-full rtl:translate-x-full md:translate-x-0 border-e border-solid border-divider',
settings.surface.value,
navigation.isOpen.value && '!translate-x-0',
!settings.prefersReducedMotion.value && 'transition-transform duration-200 ease-in-out',
Expand Down
4 changes: 3 additions & 1 deletion apps/docs/src/components/docs/DocsCodeGroup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
import { createSingle, useProxyRegistry } from '@vuetify/v0'

// Composables
import { provideCodeGroupExpand } from '@/composables/useCodeGroupExpand'
import { useSettings, type PackageManager } from '@/composables/useSettings'

// Utilities
import { cloneVNode, computed, toRef, toValue, useId, useSlots, type VNode, watch } from 'vue'
import { cloneVNode, computed, shallowRef, toRef, toValue, useId, useSlots, type VNode, watch } from 'vue'

const props = defineProps<{
noFilename?: boolean
Expand All @@ -17,6 +18,7 @@
const slots = useSlots()
const uid = useId()
const settings = useSettings()
provideCodeGroupExpand(shallowRef(false))

const single = createSingle({ mandatory: 'force', events: true })
const proxy = useProxyRegistry(single)
Expand Down
47 changes: 42 additions & 5 deletions apps/docs/src/components/docs/DocsExampleThemeMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,65 @@
import { useThemeToggleController } from '@/composables/useThemeToggle'

// Utilities
import { shallowRef, useId } from 'vue'
import { shallowRef, toRef, useId } from 'vue'

const { modesOnly = false } = defineProps<{
/** Light / dark only — no palettes, a11y themes, or custom themes. */
modesOnly?: boolean
}>()

const toggle = useThemeToggleController()
const icon = toRef(() => (
modesOnly
? (toggle.isDark.value ? 'theme-dark' : 'theme-light')
: toggle.icon.value
))
const title = toRef(() => (
modesOnly
? (toggle.isDark.value ? 'Switch to light' : 'Switch to dark')
: toggle.title.value
))

const uid = useId()
const open = shallowRef(false)

const chip = 'bg-surface-tint text-on-surface-tint hover:bg-surface-tint pa-1 inline-flex rounded-none rounded-bl-[0.375rem] rounded-tr-[0.375rem] cursor-pointer'

function onFlip () {
toggle.setMode(toggle.isDark.value ? 'light' : 'dark')
}
</script>

<template>
<Popover.Root :id="uid" v-model="open">
<AppTooltip
v-if="modesOnly"
as="span"
class="mt-[8px] me-[8px] inline-flex"
position-area="bottom"
:text="title"
>
<Button.Root
:aria-label="title"
:class="chip"
@click="onFlip"
>
<AppIcon :icon />
</Button.Root>
</AppTooltip>

<Popover.Root v-else :id="uid" v-model="open">
<AppTooltip
as="span"
class="mt-[8px] me-[8px] inline-flex"
position-area="bottom"
:text="toggle.title.value"
:text="title"
>
<Popover.Activator
aria-label="Example theme"
class="bg-surface-tint text-on-surface-tint hover:bg-surface-tint data-[state=open]:bg-surface-tint pa-1 inline-flex rounded-none rounded-bl-[0.375rem] rounded-tr-[0.375rem] cursor-pointer"
:class="[chip, 'data-[state=open]:bg-surface-tint']"
:data-state="open ? 'open' : undefined"
>
<AppIcon :icon="toggle.icon.value" />
<AppIcon :icon />
</Popover.Activator>
</AppTooltip>

Expand Down
18 changes: 12 additions & 6 deletions apps/docs/src/components/docs/DocsGenesisExample.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
} from '@/composables/usePlayground'
import { useSettings } from '@/composables/useSettings'
import { useSyncedRef } from '@/composables/useSyncedRef'
import { createThemeToggle, provideThemeToggle } from '@/composables/useThemeToggle'
import { createThemeToggle, provideThemeToggle, useThemeToggleContext } from '@/composables/useThemeToggle'

// Utilities
import { computed, onMounted, toRef } from 'vue'
Expand Down Expand Up @@ -55,16 +55,21 @@
peek?: boolean
/** Visible peek lines (default 6) */
peekLines?: number
/** Force a named theme on the preview (`data-theme`); bypasses the example theme toggle (systems pages) */
/** Force a named theme on the preview (`data-theme`); hides the palette picker unless `modesOnly` */
theme?: string
/** Default palette while following the page. Omit to track the page palette. */
palette?: Palette
/** Light / dark toggle only — used by design-system examples that own their palette */
modesOnly?: boolean
/** Skip the preview splitter */
disableResize?: boolean
}

const props = defineProps<DocsGenesisExampleProps>()

const example = createThemeToggle({ palette: props.palette })
provideThemeToggle(example)
const inherited = useThemeToggleContext()
const example = inherited ?? createThemeToggle({ palette: props.palette })
if (!inherited) provideThemeToggle(example)

const examples = useExamples()

Expand Down Expand Up @@ -141,6 +146,7 @@
:code="resolvedCode"
:collapse
data-tour="example"
:disable-resize
:file-name
:file-orders
:files="resolvedFiles"
Expand All @@ -164,8 +170,8 @@
<AppDotGrid :coverage="60" />
</template>

<template v-if="!theme" #preview-actions>
<DocsExampleThemeMenu />
<template v-if="!theme || modesOnly" #preview-actions>
<DocsExampleThemeMenu :modes-only />
</template>

<template v-if="$slots.description" #description>
Expand Down
6 changes: 3 additions & 3 deletions apps/docs/src/components/docs/DocsMarkup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
import { GnPeek } from '@paper/genesis'

// Composables
import { useCodeGroupExpand } from '@/composables/useCodeGroupExpand'
import { useSettings } from '@/composables/useSettings'
import { useSyncedRef } from '@/composables/useSyncedRef'

// Utilities
import { decodeBase64 } from '@/utilities/decodeBase64'
import { ref, toRef } from 'vue'
import { shallowRef, toRef } from 'vue'

const {
code,
Expand Down Expand Up @@ -36,8 +37,7 @@

const decodedCode = toRef(() => decodeBase64(code))

// Collapse state
const expanded = ref(false)
const expanded = useCodeGroupExpand() ?? shallowRef(false)
const lineCount = toRef(() => decodedCode.value.split('\n').length)
const shouldCollapse = toRef(() => collapse && lineCount.value > collapseLines)
const collapsedHeight = toRef(() => `${collapseLines * 1.5 + 2.5}rem`)
Expand Down
Loading
Loading