What it would take to make the table stack importable by other apps — measured against the real import graph, not estimated.
2026-09-03 · scoping, not decided · Gera: "there's other apps like the sales app or feature apps that we would want to use the components for"
The table stack is already portable. 27 files, 7,092 lines, and its only outside dependencies are react, react-dom and @phosphor-icons/react. No Next imports anywhere in the closure — so this is a packaging job, not an untangling job.
The transitive closure from the seven table components, following both @/ and relative imports:
| Directory | Files | Lines | Notable |
|---|---|---|---|
components/ui/data/ | 3 | 2,663 | DataTableCore (1,915) is the bulk of the package |
components/ui/table/ | 11 | 2,096 | FilterBar (1,060), pagination, the resize handle |
components/ui/ | 7 | 1,699 | FilterSelect (657), Tooltip, Card, Button |
lib/platform/table-state/ | 2 | 227 | URL state — no Next dependency |
lib/brand/ | 1 | 97 | z-index scale |
components/brand/, ui/feedback/ | 3 | 310 | ClaraOrb, Skeleton |
| total | 27 | 7,092 | external deps: react, react-dom, @phosphor-icons/react |
The consuming apps are separate repositories — sales-machine, gtm, propflow-ai-app. npm/pnpm workspaces only reach inside one repo, so they cannot serve this no matter how they are configured. That leaves publishing.
GitHub Packages, because the auth already exists: CI in propflow-docs already checks out propflowai with PROPFLOWAI_TOKEN. The same token installs a private package. No new secret, no new vendor.
Who can actually consume it today
camellia-sitesales-machine — the intended consumer, and not ready: it has a package.json and no React. Not checked out on the build machine as of this correction. when it existsgtm, propflow-ai-app, ccswitch — no root package.json. unknownpropflow-docs — no React, no bundler, ~180 hand-written HTML artifacts. Consumes a stylesheet and never a component. stylesheet onlySo the package today serves exactly one app: propflowai importing its own components back out of it. That is not a failure — the one-source-of-truth stylesheet, the deleted mirror machinery and the build guards all landed for that consumer alone — but it does mean nothing outside this repo is waiting, and work should be paced against the sales app becoming real rather than against a consumer that was never one.
styles.cssstyles.css onlyThe docs site is the interesting one: it stops running bin/sync-mirror and just depends on the package's stylesheet. That deletes machinery rather than adding it — the mirror becomes an npm install.
| Concern | Why it matters here | Handling |
|---|---|---|
'use client' | 19 of 21 component files carry the directive. Most bundlers strip directives during transpile, and a stripped directive turns a client component into a server component that throws on first hook. | Build with directive preservation on (tsup banner/preserveDirectives, or ship untranspiled and let consumers use transpilePackages). |
| Tailwind v4 | Utility classes survive in the JSX — concentrated in TablePagination (16), FilterBar (10), TableSearch (9). A consumer's Tailwind must SCAN the package or those classes generate nothing. | Consumers add @source pointing at the installed package. v4's CSS-first config makes this one line. |
| Tokens | Every component resolves var(--color-*) at runtime. A consumer without the tokens renders the components unstyled-ish — and an undefined custom property is an invalid declaration, so the value silently falls back to inherit rather than erroring. | @propflow/ui/styles.css ships the @theme block plus the .dt-*/.pf-* rules. Import it once. |
This is not a fresh start. A package whose components style themselves with inline JS objects cannot be themed by a consumer, and a library that injects <style> tags at runtime is a library nobody wants. Both were true of this code a week ago:
That CSS is exactly what styles.css would be. The write-up and ADR-0131 have the reasoning.
Two smaller ones worth pricing in: ClaraOrb drags brand animation into a table package (splittable into @propflow/ui and @propflow/brand if that grates), and every consumer must track the same React major — trivial today at React 19 everywhere, less so later.
What the docs site actually runs is src/components/data-table.js — 134 lines of hand-written vanilla JS implementing exactly one behaviour, sorting. Its own header is honest about it: "What is ported is the shell, the cell vocabulary, and ONE behaviour: sorting. A docs table that cannot sort is a list with lines drawn on it." Everything else the product's table does — search, dropdown filters, pagination, cold-load skeletons, the measured water-fill column resize that clones every cell off-screen — is React and is not there.
So the package can give the docs site CSS and nothing else, which is what it now does for one of six mirror blocks. Two paths actually close the original request, and they are different decisions, not different sizes of the same one:
data-table.js. Cheapest to start, and it makes the hand-mirror bigger — the thing that file's header warns about, and the reason it stopped at sorting.@propflow/ui emits a small vanilla bundle that progressively enhances a plain <table>, so the docs consume ONE artifact instead of maintaining a copy. This is the version where the packaging work pays off for docs — and it is real new work, not a flip of a switch. It also needs the geometry off Tailwind utilities first.| Phase | What lands | Status — 2026-09-04 |
|---|---|---|
| 1 — carve | packages/ui/ inside propflowai. Landed in tranches rather than as one 27-file move, by a rule the plan did not have: only components whose sole dependency is react, so the closure never grows. 5 components / 7 exports so far — TableSearch, SortableHeader, ScrollShadowContainer, HoverRow, activateOnKey. #6966, #6997, #7021 | partial — 5 of 23 |
| 2 — prove | Answered, but not the way this row predicted. The app still resolves source: CI restores node_modules from a lockfile-keyed cache and skips npm ci on a hit, so an install-built dist would be absent on exactly those runs. The question the flip existed to answer — does the built package work for someone who installs it — is answered directly instead: the guard imports dist/index.js and renders it through React, and compiles a standalone consumer against dist/index.d.ts. 'use client' does not survive a bundle unaided; esbuild drops it and a banner re-establishes it. #7014, #7034 | answered |
| 3 — publish | GitHub Packages, versioned, CI release on tag. Deferred, and no longer next. This row said camellia-site installs it; camellia-site is not a consumer, and sales-machine has no React yet — so there is nobody to publish to. Still needs exports/types flipped to dist, and the component geometry converted off Tailwind utility classes, or an installing app renders it unstyled. | no consumer waiting |
| 4 — docs switches | propflow-docs consumes the package's stylesheet. First of six mirror blocks graduated: the .dt-search* family stopped being text-anchored out of the app's 2,000-line globals.css and became a copy of packages/ui/styles.css. This was also a repair — moving those rules broke the anchor sync-mirror depended on. The other five still anchor into globals.css, and the docs consume a stylesheet and never a component. | 1 of 6 blocks |
| 5 — the design system (new — not in the original plan) | The blocker nobody costed. DataTable's closure is 23 files and its only external deps are react, react-dom, @phosphor-icons/react — so it is portable. But five of those files are app-wide primitives: Card (214 importers), z-index (122), Skeleton (122), Tooltip (104), Button (92). Moving "the table" means moving the design system, ~650 import sites. The alternative is inverting the dependency — the table takes chrome as props — which is a smaller diff and an API change. | the real decision |
Original numbers measured 2026-09-03 against origin/main: closure by following imports from the seven table components, line counts per file, 'use client' by reading first lines, consumer stacks from each repo's package.json over the GitHub API.
Re-measured 2026-09-04 against origin/main after #6966, #6997, #7014, #7021 and #7034: the remaining DataTable closure is 23 files, importer counts by git grep over src + agents, consumer stacks by reading each repo on the build machine. The consumer correction is the owner's, not a measurement — and it is the one that re-ordered everything below it.