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
145 changes: 145 additions & 0 deletions .github/build_changelog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
#!/usr/bin/env python3
"""Build CHANGELOG.md from changelog.d/ fragments."""

import json
import os
import sys
from datetime import date

CHANGELOG_DIR = "changelog.d"
CHANGELOG_PATH = "CHANGELOG.md"
PACKAGE_PATH = "package.json"

TYPE_HEADING = {
"added": "Added",
"changed": "Changed",
"fixed": "Fixed",
"removed": "Removed",
"breaking": "Breaking",
}
TYPE_ORDER = ["Added", "Changed", "Fixed", "Removed", "Breaking"]


def get_current_version():
with open(PACKAGE_PATH) as f:
data = json.load(f)
return data["version"]


def get_previous_version():
"""Read the most recent version from existing CHANGELOG.md."""
if not os.path.exists(CHANGELOG_PATH):
return "0.0.0"
with open(CHANGELOG_PATH) as f:
for line in f:
if line.startswith("## ["):
return line.split("[")[1].split("]")[0]
return "0.0.0"


def read_fragments():
"""Read all fragments and group by type heading."""
groups = {}
for fname in sorted(os.listdir(CHANGELOG_DIR)):
if fname == ".gitkeep":
continue
parts = fname.rsplit(".", 2)
if len(parts) != 3 or parts[2] != "md":
continue
fragment_type = parts[1]
heading = TYPE_HEADING.get(fragment_type, fragment_type.capitalize())
with open(os.path.join(CHANGELOG_DIR, fname)) as f:
content = f.read().strip()
if content:
groups.setdefault(heading, []).append(content)
return groups


def build_section(version, groups):
"""Build a Keep a Changelog section."""
lines = [f"## [{version}] - {date.today().isoformat()}", ""]
for heading in TYPE_ORDER:
if heading not in groups:
continue
lines.append(f"### {heading}")
lines.append("")
for entry in groups[heading]:
for raw_line in entry.splitlines():
raw_line = raw_line.strip()
if not raw_line:
continue
if not raw_line.startswith("- "):
raw_line = f"- {raw_line}"
lines.append(raw_line)
lines.append("")
return "\n".join(lines)


def update_changelog(new_section, version, old_version):
"""Prepend new section to CHANGELOG.md and add comparison link."""
if os.path.exists(CHANGELOG_PATH):
with open(CHANGELOG_PATH) as f:
content = f.read()
else:
content = "# Changelog\n"

# Insert new section before the first existing ## entry
marker = "\n## ["
pos = content.find(marker)
if pos == -1:
# No existing entries — append after header
content = content.rstrip() + "\n\n" + new_section + "\n"
else:
header = content[: pos + 1] # include the trailing newline
rest = content[pos + 1 :]
content = header + new_section + "\n" + rest

# Add comparison link at the top of the link reference section
link = (
f"[{version}]: https://github.com/PolicyEngine/"
f"policyengine-ui-kit/compare/{old_version}...{version}"
)
lines = content.split("\n")
new_lines = []
link_inserted = False
for line in lines:
if not link_inserted and line.startswith("[") and "]: https://" in line:
new_lines.append(link)
link_inserted = True
new_lines.append(line)
if not link_inserted:
new_lines.append("")
new_lines.append(link)
content = "\n".join(new_lines)

with open(CHANGELOG_PATH, "w") as f:
f.write(content)


def delete_fragments():
"""Remove consumed fragments, keeping .gitkeep."""
for fname in os.listdir(CHANGELOG_DIR):
if fname == ".gitkeep":
continue
os.remove(os.path.join(CHANGELOG_DIR, fname))


def main():
has_fragments = any(f != ".gitkeep" for f in os.listdir(CHANGELOG_DIR))
if not has_fragments:
print("No changelog fragments found. Nothing to build.")
sys.exit(0)

version = get_current_version()
old_version = get_previous_version()
groups = read_fragments()

new_section = build_section(version, groups)
update_changelog(new_section, version, old_version)
delete_fragments()

print(f"Built changelog for version {version}")


if __name__ == "__main__":
main()
79 changes: 79 additions & 0 deletions .github/bump_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Bump version in package.json based on changelog.d/ fragments."""

import json
import os
import sys

CHANGELOG_DIR = "changelog.d"
PACKAGE_PATH = "package.json"

# Lower number = higher priority bump
BUMP_PRIORITY = {
"breaking": 0, # major
"added": 1, # minor
"removed": 1, # minor
"changed": 2, # patch
"fixed": 2, # patch
}


def get_current_version():
with open(PACKAGE_PATH) as f:
data = json.load(f)
return data["version"]


def get_bump_level():
"""Scan fragment filenames to determine the highest-priority bump type."""
level = None
for fname in os.listdir(CHANGELOG_DIR):
if fname == ".gitkeep":
continue
# Expected format: {name}.{type}.md
parts = fname.rsplit(".", 2)
if len(parts) != 3 or parts[2] != "md":
continue
fragment_type = parts[1]
priority = BUMP_PRIORITY.get(fragment_type, 2) # default to patch
if level is None or priority < level:
level = priority
return level


def compute_new_version(version, level):
major, minor, patch = map(int, version.split("."))
if level == 0:
return f"{major + 1}.0.0"
elif level == 1:
return f"{major}.{minor + 1}.0"
else:
return f"{major}.{minor}.{patch + 1}"


def main():
fragments = [f for f in os.listdir(CHANGELOG_DIR) if f != ".gitkeep"]
if not fragments:
print("No changelog fragments found. Nothing to bump.")
sys.exit(0)

old_version = get_current_version()
level = get_bump_level()
if level is None:
print("No valid fragments found. Nothing to bump.")
sys.exit(0)

new_version = compute_new_version(old_version, level)

with open(PACKAGE_PATH) as f:
data = json.load(f)
data["version"] = new_version
with open(PACKAGE_PATH, "w") as f:
json.dump(data, f, indent=2)
f.write("\n")

print(f"Bumped version: {old_version} → {new_version}")


if __name__ == "__main__":
main()
66 changes: 66 additions & 0 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Version, Changelog, and Publish

on:
push:
branches: [main]

jobs:
release:
name: Bump version, build changelog, publish to npm
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, 'Release @policyengine/ui-kit')"

steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.POLICYENGINE_GITHUB }}

- name: Check for changelog fragments
id: check
run: |
FRAGMENTS=$(ls changelog.d/ | grep -v .gitkeep | wc -l)
echo "count=$FRAGMENTS" >> "$GITHUB_OUTPUT"

- name: Bump version
if: steps.check.outputs.count != '0'
run: python .github/bump_version.py

- name: Build changelog
if: steps.check.outputs.count != '0'
run: python .github/build_changelog.py

- name: Set up Bun
if: steps.check.outputs.count != '0'
uses: oven-sh/setup-bun@v2

- name: Install dependencies
if: steps.check.outputs.count != '0'
run: bun install --frozen-lockfile

- name: Build
if: steps.check.outputs.count != '0'
run: bun run build

- name: Set up Node for npm publish
if: steps.check.outputs.count != '0'
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org

- name: Publish to npm
if: steps.check.outputs.count != '0'
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Commit and push
if: steps.check.outputs.count != '0'
uses: EndBug/add-and-commit@v9
with:
message: "Release @policyengine/ui-kit"
default_author: github_actions
add: |
package.json
CHANGELOG.md
changelog.d/
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Changelog
20 changes: 15 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,39 @@
# @policyengine/ui-kit

PolicyEngine UI kit — design tokens, Tailwind CSS v4 theme, and React 19 components for dashboards and calculators.
PolicyEngine UI kit — CSS-first design tokens, Tailwind CSS v4 theme, shadcn/ui primitives, and React 19 components for dashboards and calculators.

## Commands

- Install: `bun install`
- Build: `bun run build`
- Test: `bun run test`
- Type check: `bun run typecheck`
- Demo: `bun run dev:demo`

## Architecture

- **Vite library mode** — builds ESM + CJS + types + styles.css
- **Tailwind CSS v4** with `tw:` prefix (mirrors policyengine-app-v2)
- **Tailwind CSS v4** with standard class names (no prefix)
- **shadcn/ui** pattern for primitives (Button, Badge, Card, Tabs)
- **CVA** (class-variance-authority) for component variants
- **Recharts** for chart components (peer dependency)

## Design tokens

Tokens are in `src/tokens/` — colors, typography, spacing, charts. These are the source of truth for all PolicyEngine applications. The same values appear as CSS custom properties in `src/app.css` via the `@theme` block.
Tokens are in `src/theme/tokens.css` — the single source of truth for all frontend projects. This CSS file defines:

1. **Layer 1 (`:root`)**: shadcn/ui semantic variables (`--primary`, `--background`, `--chart-1`, etc.)
2. **Layer 2 (`@theme inline`)**: Bridges `:root` vars to Tailwind utilities (`bg-primary`, `text-foreground`)
3. **Layer 3 (`@theme`)**: Brand palette (`bg-teal-500`, `text-gray-600`), font sizes, spacing, breakpoints

Consumers import: `@import "@policyengine/ui-kit/theme.css";`

## Styling rules

- All Tailwind classes use `tw:` prefix (e.g. `tw:bg-primary-500`)
- Use standard Tailwind classes (`bg-primary`, `text-muted-foreground`, `border-border`, `rounded-lg`)
- Use brand palette classes for specific colors (`bg-teal-500`, `text-gray-600`)
- Use `cn()` from `src/utils/cn.ts` for class merging
- Never hardcode hex colors — use token classes or imports
- Recharts charts use CSS vars directly: `fill="var(--chart-1)"`
- Never hardcode hex colors in component code
- Sentence case for all UI text
- Every component accepts `className` and `styles` props
1 change: 1 addition & 0 deletions changelog.d/pe-token-alignment.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Align with PE design system: remove tw: prefix, adopt pe-* token naming convention, bridge @theme values to @policyengine/design-system CSS variables.
18 changes: 18 additions & 0 deletions components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/theme/tokens.css",
"baseColor": "neutral",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/utils",
"ui": "@/primitives",
"lib": "@/utils"
}
}
Loading
Loading