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
41 changes: 41 additions & 0 deletions .docker/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
server {
listen 80;
server_name _;

root /usr/share/nginx/html;
index index.html;

# Health probe for Coolify. Kept trivial and unauthenticated so the proxy
# can check the container without an SSO session.
location = /healthz {
access_log off;
add_header Content-Type text/plain;
return 200 "ok\n";
}

# Vite writes a content hash into every asset filename, so these can be
# cached indefinitely. `^~` stops nginx from also testing the regex block
# below, which would otherwise claim any hashed `.json` asset.
location ^~ /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}

# The entry pages and the story index must never be served stale: after a
# redeploy a cached index would point at asset names that no longer exist.
location ~* \.(html|json)$ {
add_header Cache-Control "no-cache";
try_files $uri =404;
}

# index.html for the manager, iframe.html for the stories themselves.
location / {
try_files $uri $uri/ /index.html;
}

gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
}
17 changes: 17 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Never ship a host build into the image — it would mask a broken build.
node_modules
dist
storybook-static
*.tsbuildinfo

.git
.github
.idea
.claude
.env
.env.*

docs
specs
*.md
!README.md
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ sbom/
*.tsbuildinfo
.env
.claude/settings.local.json

# Storybook static build
storybook-static/
17 changes: 17 additions & 0 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { StorybookConfig } from "@storybook/react-vite";
import tailwindcss from "@tailwindcss/vite";
import { mergeConfig } from "vite";

const config: StorybookConfig = {
stories: ["../stories/**/*.stories.@(ts|tsx)"],
addons: ["@storybook/addon-docs", "@storybook/addon-a11y"],
framework: {
name: "@storybook/react-vite",
options: {},
},
// Tailwind only ever runs here. The published package ships raw `.tsx` and
// `brand.css`; compiling utilities stays the consuming app's job.
viteFinal: (viteConfig) => mergeConfig(viteConfig, { plugins: [tailwindcss()] }),
};

export default config;
14 changes: 14 additions & 0 deletions .storybook/manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { addons } from "storybook/manager-api";
import { create } from "storybook/theming/create";

// Brand the catalogue itself, so the two consumer teams can tell at a glance
// which library they are looking at.
addons.setConfig({
theme: create({
base: "light",
brandTitle: "Open Elements UI",
brandUrl: "https://github.com/OpenElementsLabs/open-elements-ui",
colorPrimary: "#5cba9e",
colorSecondary: "#020144",
}),
});
14 changes: 14 additions & 0 deletions .storybook/preview.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@import "tailwindcss";
@import "../src/styles/brand.css";

@plugin "@tailwindcss/typography";

/*
* The same content configuration a consuming app needs. Spec 001 recorded this
* as an unverified precondition: the library ships utility classes as source
* text, so they only become real CSS once something scans `src/`. If a story
* renders unstyled, the assumption was wrong — and it surfaces here rather
* than in an application.
*/
@source "../src";
@source "../stories";
21 changes: 21 additions & 0 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Preview } from "@storybook/react-vite";
import type { ReactNode } from "react";
import "./preview.css";

const preview: Preview = {
parameters: {
controls: { expanded: true },
layout: "padded",
},
decorators: [
// Stand in for the consuming app's root element, which is where the brand
// body font and base colours are applied.
(Story: () => ReactNode) => (
<div className="font-body text-foreground bg-background">
<Story />
</div>
),
],
};

export default preview;
38 changes: 38 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# syntax=docker/dockerfile:1

# The component showcase: a static Storybook build served by nginx.
#
# A Dockerfile rather than Coolify's static build pack, so the build command and
# the publish directory stay in version control and the whole thing is
# reproducible locally with `docker build . && docker run -p 8080:80 <image>`.

# --- build ---------------------------------------------------------------------
FROM node:24-alpine AS build

WORKDIR /app

# Node 24 to match .nvmrc; pnpm comes from the packageManager field.
RUN corepack enable

ENV CI=true
ENV STORYBOOK_DISABLE_TELEMETRY=1
# Corepack fetches the pinned pnpm on first use; never wait for a prompt.
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0

# Manifests first, so editing a story does not reinstall the toolchain.
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile

COPY . .
RUN pnpm run build-storybook

# --- serve ---------------------------------------------------------------------
FROM nginx:stable-alpine AS serve

COPY .docker/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/storybook-static /usr/share/nginx/html

EXPOSE 80

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD wget -q --spider http://localhost/healthz || exit 1
49 changes: 45 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,63 @@ Import brand CSS in your app's stylesheet:
@import "@open-elements/ui/src/styles/brand.css";
```

Because this package ships raw `.tsx`, its utility classes reach your app as source text. Tailwind
only turns them into CSS if it scans the library, so the consuming app must point at it and load the
typography plugin that `MarkdownView` relies on:

```css
@import "tailwindcss";
@import "@open-elements/ui/src/styles/brand.css";
@plugin "@tailwindcss/typography";
@source "../node_modules/@open-elements/ui/src";
```

Without the `@source` line, components render unstyled — task list checkboxes pick up a `prose`
bullet, for instance. The [component showcase](#component-showcase) uses the same configuration and
asserts the result, so a break in this contract surfaces there.

## Translations

```typescript
import { de, en } from "@open-elements/ui";
```

## Component Showcase

Every component can be opened in isolation, with its props adjustable at runtime, in a Storybook
catalogue:

```bash
pnpm storybook # dev server on http://localhost:6006
pnpm build-storybook # static build into storybook-static/
```

Stories live in [`stories/`](stories/), deliberately outside `src/`: `files` in `package.json`
publishes all of `src/`, so a colocated story would ship in the tarball and break a consumer's `tsc`
on unresolvable `@storybook/*` imports. Nothing under `src/` imports Storybook, and Tailwind is a
devDependency that no build script consumes — the published package is unaffected.

Stories carry `play` functions, which run in a real browser and cover what jsdom can only check
indirectly: the toolbar allowlist, the task-list creation gate under actual keystrokes, the
checkbox lifecycle after actual clicks, and that the Tailwind utilities the components rely on
resolve to real styles. They are additive; the vitest suites are unchanged.

`MarkdownEditor` and `MarkdownView` establish the pattern. The remaining components follow
incrementally.

Deployment of the showcase is documented in
[docs/showcase-deployment.md](docs/showcase-deployment.md).

## Software Bill of Materials (SBOM)

Every release publishes two [CycloneDX](https://cyclonedx.org/) 1.7 SBOMs as assets on its
[GitHub Release](https://github.com/OpenElementsLabs/open-elements-ui/releases), so a specific
published version can be obtained without an `npm install`:

| Asset | Contents | Authoritative? |
| --- | --- | --- |
| `sbom.cdx.json` | Runtime dependencies (transitive) plus the library's peer dependencies | **Yes** — use this for supplier assessments (Cyber Resilience Act) |
| `sbom-dev.cdx.json` | The build toolchain (`devDependencies`) | No — provided for transparency only |
| Asset | Contents | Authoritative? |
| ------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------ |
| `sbom.cdx.json` | Runtime dependencies (transitive) plus the library's peer dependencies | **Yes** — use this for supplier assessments (Cyber Resilience Act) |
| `sbom-dev.cdx.json` | The build toolchain (`devDependencies`) | No — provided for transparency only |

Both are generated locally with the pinned `pnpm` (`pnpm sbom`) and verified in CI on every pull
request, so a dependency change that breaks the SBOM turns the build red. A release cannot ship without
Expand Down
Loading
Loading