Skip to content
Merged
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
90 changes: 81 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,90 @@
name: ci
# `push` limite a main, sinon chaque branche de PR declenche DEUX fois le meme job — une
# fois pour le push, une fois pour la PR — sur exactement le meme commit.

# `push` reste limite a main pour ne pas executer deux fois la verification
# d'une branche qui a deja une pull request ouverte.
on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
verifie:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: 24, cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: pnpm test
- uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
with:
# Bun 1.4.0 est encore publie sur le canal canary.
bun-version: canary
- run: bun install --frozen-lockfile
- run: bun run test
- run: bun run build
- run: bun run pack:check
- uses: actions/upload-artifact@v4
with:
name: app-dist
path: dist/app
if-no-files-found: error
retention-days: 1

apercu:
if: >-
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository
needs: verifie
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
pull-requests: write
steps:
- uses: actions/download-artifact@v4
with:
name: app-dist
path: dist/app
- name: Deploie l'apercu Cloudflare Pages
id: deploy
uses: cloudflare/wrangler-action@v4
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist/app --project-name=bloub-app --branch=pr-${{ github.event.pull_request.number }}
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
- name: Publie le lien dans la pull request
env:
GH_TOKEN: ${{ github.token }}
PREVIEW_URL: ${{ steps.deploy.outputs.pages-deployment-alias-url }}
run: >-
gh pr comment "${{ github.event.pull_request.number }}"
--repo "${{ github.repository }}"
--edit-last
--create-if-none
--body "Apercu Cloudflare Pages : [${PREVIEW_URL}](${PREVIEW_URL})"

production:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: verifie
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
steps:
- uses: actions/download-artifact@v4
with:
name: app-dist
path: dist/app
- name: Deploie Cloudflare Pages en production
uses: cloudflare/wrangler-action@v4
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist/app --project-name=bloub-app --branch=main
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,5 +90,5 @@ fixed, and the measurement is in the commit that fixes it.
- `StateDef.hint`: 15 hardcoded French strings nothing read, inside a type meant to
become public.

[unreleased]: https://github.com/jeremy-prt/bloub/compare/v0.1.1...HEAD
[0.1.1]: https://github.com/jeremy-prt/bloub/releases/tag/v0.1.1
[unreleased]: https://github.com/NorbertBodziony/bloub/compare/v0.1.1...HEAD
[0.1.1]: https://github.com/NorbertBodziony/bloub/releases/tag/v0.1.1
24 changes: 12 additions & 12 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
## Commands

```bash
pnpm dev # 5190 (set in vite.config.ts, mirrored in .claude/launch.json)
pnpm test # vitest
pnpm build # vue-tsc --noEmit && vite build
bun run dev # 5190 (set in vite.config.ts, mirrored in .claude/launch.json)
bun run test # Vitest
bun run build # type check, demo build, and package build
```

Vue 3.5 + Vite 8 + TS strict + Tailwind 4 (`@tailwindcss/vite` plugin, no
`tailwind.config.js`), pnpm.
SolidJS + Vite 8 + TS strict + Tailwind 4 (`@tailwindcss/vite` plugin, no
`tailwind.config.js`), Bun.

Style: 2 spaces, single quotes, **no semicolons**, comments in French. No ESLint
and no Prettier: `vue-tsc` is the only gate, so run `pnpm build` before
and no Prettier: strict TypeScript is the source gate, so run `bun run build` before
concluding.

## The most important rule
Expand Down Expand Up @@ -106,7 +106,7 @@ Details and the reasoning behind each are in [docs/](docs/):
arrow-key navigation and focus moved into the menu on open, and they stop exposing the
children as ordinary buttons. Three popups declared them and implemented none of it, so
the Tab order didn't match what was announced — they are plain button lists now, with
`aria-haspopup="true"` and `aria-expanded`. `Settings.vue` shows the other route: a real
`aria-haspopup="true"` and `aria-expanded`. `Settings.tsx` shows the other route: a real
`radiogroup` with a moving `tabindex`. Pick one, never the label alone.
- **64rem is the only breakpoint, and it separates two different layouts, not two sizes.**
Above it the scene is the three-column grid and the page never scrolls (`#app { overflow:
Expand Down Expand Up @@ -146,11 +146,11 @@ run it, the component's API. Don't duplicate it here.

## Tests

`pnpm test` runs in `node` by default. **One file asks for a DOM** and says so on its first
line (`// @vitest-environment happy-dom`): `ui/capture.test.ts`, which mounts `BloubBot.vue`
`bun run test` runs in `node` by default. DOM tests ask for `happy-dom` on their first
line (`// @vitest-environment happy-dom`). `ui/capture.test.ts` mounts `BloubBot.tsx`
to check the off-screen player — the exported render must be the component's own, not a
second drawing built beside it. That is also why `vitest.config.ts` carries the Vue plugin.
Keep the DOM per-file: a global DOM environment would slow the whole suite for one test.
second drawing built beside it. That is also why `vitest.config.ts` carries the Solid plugin.
Keep the DOM per-file: a global DOM environment would slow the rest of the suite.

`capture.test.ts` is the one that catches what nothing else can — the export defects are
invisible short of stepping through an MP4 frame by frame.
Expand All @@ -166,7 +166,7 @@ are what `engine.sample(1)` returns for `idle`, byte for byte. `favicon.ico` and
`apple-touch-icon.png` are rasterised from it.

`docs/demo.gif` and `docs/states.png` are the same idea: rendered by walking
`engine.sample(t)` and writing the SVG layers in `BloubBot.vue`'s order, then
`engine.sample(t)` and writing the SVG layers in `BloubBot.tsx`'s order, then
`rsvg-convert` + `ffmpeg`. They are **not** browser captures: the browser pane
suspends `requestAnimationFrame` when hidden, so an animation can't be captured
there at all. To redo them, drive the engine, don't reach for a screenshot.
Expand Down
51 changes: 35 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# bloub

[Live demo](https://bloub-app.pages.dev) ·
[![CI](https://github.com/NorbertBodziony/bloub/actions/workflows/ci.yml/badge.svg)](https://github.com/NorbertBodziony/bloub/actions/workflows/ci.yml)

An SVG recreation of the x.ai bot avatar: **one filled black shape** that morphs
between 14 states, **two white shapes** for the eyes that morph independently, on
a plain background. No animation library.
Expand All @@ -9,19 +12,20 @@ a plain background. No animation library.
## Running it

```bash
pnpm install
pnpm dev
bun install
bun run dev
```

Then open http://localhost:5190.

```bash
pnpm test # vitest
pnpm build # vue-tsc --noEmit && vite build
bun run test # Vitest
bun run build # strict type check, demo build, and package build
```

Vue 3, Vite, TypeScript, Tailwind 4. No ESLint and no Prettier: `vue-tsc` is the
only gate, so run `pnpm build` before you call something done.
SolidJS, Vite, TypeScript, Tailwind 4, and Bun. No ESLint and no Prettier:
strict TypeScript is the source gate, so run `bun run build` before you call
something done.

## What's in it

Expand Down Expand Up @@ -80,21 +84,36 @@ the DOM-less test suite possible.
| [docs/export.md](docs/export.md) | Exporting to SVG, PNG, GIF and MP4 |
| [docs/i18n.md](docs/i18n.md) | The hand-rolled translation layer |

## Using the component
## Using the package

```bash
bun add bloub solid-js
```

```tsx
import { createSignal } from 'solid-js'
import { BloubBot, defaultCycle, type BloubBotRef } from 'bloub'

const [playing, setPlaying] = createSignal(false)
let bot: BloubBotRef | undefined

```vue
<BloubBot v-model:block="block" v-model:state="state" v-model:playing="playing" />
<BloubBot state="orbit" :size="120" :frozen-at="1.2" />
<BloubBot
ref={(value) => (bot = value)}
cycle={defaultCycle().blocks}
playing={playing()}
onPlayingChange={setPlaying}
/>
```

`block` is the playback cursor: a montage can play the same state twice, so the
index is what identifies where you are; `state` follows it as an output. Pass
`frozenAt` and the component renders one exact frame with no animation loop, which
is how the thumbnails and the state board are drawn.
`block` is the playback cursor. A montage can play the same state twice, so the
index identifies the current item. `state` follows it as an output. Pass
`frozenAt` to render one exact frame with no animation loop. The component also
exports a ref with `svg`, `seek(index, offset?)`, and `renderAt(seconds)`.

Props: `size`, `shape`, `color`, `expression`, `paper`, `frozenAt`, `cycle`,
`follow`, `gaze`. Models: `block`, `state`, `playing`, `elapsed`. See
[BloubBot.vue](src/components/BloubBot.vue) for the details.
`follow`, `gaze`, `ariaLabel`, `block`, `state`, `playing`, and `elapsed`. Each
playback value has a matching `on...Change` callback. See
[BloubBot.tsx](src/components/BloubBot.tsx) for the details.

## Changes

Expand Down
Loading
Loading