Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
cfad200
docs: add propel icons design spec
anmolsinghbhatia Aug 17, 2026
41f21db
docs: add propel icons implementation plan
anmolsinghbhatia Aug 17, 2026
d817d3f
feat: add icon SVG source of truth from Figma
anmolsinghbhatia Aug 17, 2026
e57dcab
docs: emit both glyphs for shared icon names with category-suffixed e…
anmolsinghbhatia Aug 17, 2026
9f32329
feat: add icon component generator and generated output
anmolsinghbhatia Aug 17, 2026
ee28676
docs: ignore defs/clipPath fills when detecting monochrome icons
anmolsinghbhatia Aug 17, 2026
505b126
fix: ignore defs/clipPath fills when detecting monochrome icons
anmolsinghbhatia Aug 17, 2026
3e7e6ff
feat: publish @makeplane/propel/icons subpath with drift check
anmolsinghbhatia Aug 17, 2026
cd595b5
test: add generated icons invariants suite
anmolsinghbhatia Aug 17, 2026
6376b87
docs: document generated icons protocol in AGENTS.md
anmolsinghbhatia Aug 17, 2026
715c38c
fix: correct brand-mark color detection and enforce icon color policy
anmolsinghbhatia Aug 17, 2026
e86e4db
fix: keep third-party brand icon palettes verbatim
anmolsinghbhatia Aug 17, 2026
ef53ff6
feat: add iconography gallery story
anmolsinghbhatia Aug 17, 2026
0466338
fix: name the @ icon sources at-filled/at-outline
anmolsinghbhatia Aug 18, 2026
9d33fbc
fix: namespace generated icon ids and harden icon generation
anmolsinghbhatia Aug 18, 2026
c442a99
build: keep the "use client" banner off icon chunks
anmolsinghbhatia Aug 18, 2026
1ddaa14
fix: theme monochrome brand marks and harden icon generation
anmolsinghbhatia Aug 18, 2026
61d17f0
fix: name alternate icon variants -alt instead of -2
anmolsinghbhatia Aug 18, 2026
49db210
docs: correct the iconography story's theming description
anmolsinghbhatia Aug 18, 2026
b803388
fix: keep the icon state suffix last in collision-disambiguated names
anmolsinghbhatia Aug 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,6 @@ packages/propel/.ds-shim/
.ds-sync/
ds-bundle/
packages/propel/.ds-storybook/

# Icon generation scratch dir (swapped into src/icons on success)
packages/propel/.icons.tmp/
8 changes: 7 additions & 1 deletion .oxfmtrc.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
{
"ignorePatterns": ["**/dist/**", "**/.claude/**", "**/.agents/**"],
"ignorePatterns": [
"**/dist/**",
"**/.claude/**",
"**/.agents/**",
"**/src/icons/**",
"**/.icons.tmp/**"
],
"sortTailwindcss": {
"functions": ["cva", "cx"],
"stylesheet": "packages/propel/tailwind.css"
Expand Down
4 changes: 3 additions & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"**/.storybook/**",
"**/storybook-static/**",
"**/vitest.config.ts",
"**/tsdown.config.ts"
"**/tsdown.config.ts",
"**/src/icons/**",
"**/.icons.tmp/**"
],
"rules": {
"propel/prefer-tailwind-v4-shorthand": "error",
Expand Down
113 changes: 113 additions & 0 deletions docs/propel-icons/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Propel icons — design

Ship Plane's icon set (741 SVGs from the Figma [Foundations / Iconography](https://www.figma.com/design/pr5DXHRES2d5QM1qMlr0io/Foundations?node-id=1612-6650) section) as tree-shakeable React components under a new `@makeplane/propel/icons` subpath.

## Decisions

| Question | Decision |
| --- | --- |
| Where do icons live? | Inside `packages/propel`, published as the `@makeplane/propel/icons` subpath |
| Import API | Named per-icon exports from one entry: `import { StickyNoteOutline } from "@makeplane/propel/icons"` |
| Codegen model | SVGs committed as source of truth; generator output (`.tsx`) also committed; CI drift check |
| Color | Monochrome icons → `currentColor`; multi-color brand marks keep their palette |
| Default size | `1em` (tracks font-size, overridable via `className`/`width`/`height`) |

### Why committed output instead of build-time generation

- `tsdown.config.ts` builds its entry map by scanning real folders on disk; typecheck, Storybook, Vitest, and the editor TS server all need the component files to exist. Build-time generation would put a "generate first" step in front of every workflow.
- Committed output makes icon changes reviewable in PRs: a Figma re-sync shows exactly which components changed.
- Staleness is prevented by a `check:icons` task that re-runs the generator and fails on any drift between the SVGs and the committed output — the guarantee of build-time generation without its DX cost.

Build-time generation wins when output is huge, per-environment, or constantly churning; none of that applies to an icon set that changes only when design ships new icons.

## File layout

```
packages/propel/
icons/ # SVG source of truth (moved from repo root), one folder per Figma category
actions/add-outline.svg
layouts/board-filled.svg
scripts/generate-icons.mjs # the generator
src/icons/ # generated output, committed — never hand-edited
index.ts # barrel: export * from every icon module
actions/add-outline.tsx
```

- Categories (from the Figma section headers): `workspace-features`, `sub-brands`, `project-features`, `layouts`, `formatting-and-editors`, `properties`, `actions`, `charts`, `arrows`, `miscellaneous`, `other-products`, `placeholder`.
- Categories exist as folders for browsing and diffs only; they are **not** part of the import path. Consumers only ever import from `@makeplane/propel/icons`.
- Wiring: one static `"./icons"` entry in the package.json `exports` map, one named `icons/index` entry in `tsdown.config.ts`. With `unbundle: true`, every icon module stays its own output chunk, so per-icon tree-shaking works through the barrel.

## Generated component template

```tsx
// Generated by scripts/generate-icons.mjs — do not edit.
import type { SVGProps } from "react";

export function AddOutline(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
width="1em"
height="1em"
fill="none"
aria-hidden
{...props}
>
<path fill="currentColor" d="…" />
</svg>
);
}
```

- Plain function component with an `SVGProps<SVGSVGElement>` spread — React 19, so `ref` is a normal prop; no `forwardRef`.
- `aria-hidden` by default (icons are decorative); consumers override via the spread when an icon is meaningful.
- Component names are PascalCased from the SVG filename (`h1-outline` → `H1Outline`, `plane-ai-2` → `PlaneAi2`).

## Generator (`pnpm gen:icons`)

`packages/propel/scripts/generate-icons.mjs`, run with the repo's script runner. Pipeline per SVG:

1. **SVGO** cleanup: strip `width`/`height` (keep `viewBox`), strip `id`s and editor metadata, merge/minify paths.
2. **Color pass**: count distinct paint values (`fill`/`stroke`). Exactly one → replace with `currentColor` (monochrome icon). More than one → leave untouched (brand marks in `sub-brands` / `other-products` such as Instagram, MS Word).
3. **SVGR** SVG → TSX with the template above, TypeScript output.
4. **Barrel**: regenerate `src/icons/index.ts` with `export * from "./<category>/<name>"` lines, sorted for stable diffs.

Error handling:

- **Cross-category duplicates**: the Figma section repeats 14 filenames across two categories. Investigation showed 12 of the 14 are **genuinely different glyphs that coincidentally share a name** (e.g. `delete-filled` is a trash can in `actions` but an octagon-X in `arrows`); only the `toggle-off-*` pair is the same glyph (normalized in source to the `properties` copy). Policy: when two source SVGs share a filename, the generator compares post-SVGO content. Byte-identical → keep the first by category order, skip the rest. Different content → emit **both**; the occurrence in the later category gets its category appended to the component name (`arrows/delete-filled.svg` → `DeleteFilledArrows`) so the flat named-export API stays collision-free while filenames keep tracking Figma. A final uniqueness check over all export names fails the build if any collision survives. Expected output: 739 components (741 SVGs − 2 deduped toggle-off icons).
- Any SVG that fails to parse fails the whole run with its path.

## Scripts and CI

Following the repo's `check:*` / `fix:*` convention (turbo tasks + per-package scripts):

- `gen:icons` — run the generator, write into `src/icons/`.
- `check:icons` — run the generator into a temp dir, diff against `src/icons/`, exit non-zero on any difference. Added to the `check` umbrella and `turbo.json`.

Re-sync flow when design ships new icons: export SVGs into `packages/propel/icons/<category>/` → `pnpm gen:icons` → commit SVGs + generated output together.

## Cleanup before first generation

- The Figma MCP export wraps each icon in section-background junk; a cleaning script strips it. 14 of the 741 SVGs came through with a different structure ("no wrapper group") and were saved raw — these get fixed before the first generation: `group-filled`, `group-outline`, `unsyn-outline`, `unsyn-filled`, `anchor-link-filled`, `anchor-link-outline` (miscellaneous), `intake-outline`, `intake-filled` (project-features), `ms-word`, `ms-excel`, `ms-powerpoint`, `instagram`, `jira`, `codex` (other-products).
- All 741 manifest entries are verified on disk.

## Docs

`packages/propel/AGENTS.md` gets a short icons section: `src/icons/` is generated — never hand-edit; run `pnpm gen:icons` after changing SVGs; `lucide-react` remains the icon source inside `components` for now (migrating propel's own components to these icons is explicitly out of scope for this feature).

## Testing

One Vitest suite (`src/internal/icons.test.ts` — it cannot live in `src/icons/`, which is generated and drift-checked byte-for-byte):

- The barrel exports the expected number of components (count derived from the SVG source tree, not hardcoded).
- Every export renders an `<svg>` element with a `viewBox`.
- Monochrome icons contain no hardcoded hex fills (everything is `currentColor`).

## Out of scope

- Migrating propel `components` off `lucide-react`.
- A Storybook gallery page for the icon set (nice-to-have follow-up).
- Automated Figma → repo sync (the manifest + download tooling from this effort lives in session scratchpad; a committed sync script can be a follow-up).
Loading
Loading