C# / WPF targeting Windows 10 or newer (.NET 10, net10.0-windows).
Built with Visual Studio 2022 on Windows; code can be authored on macOS via
dotnet CLI + VS Code (C# Dev Kit), but the app can only build and run on Windows.
InterlinedList uses the Strata design system: teal #184860 structure,
green #2FA877 actions, amber #F0A830 for live/Dig, soft-sand light #F4EEE2
and near-black dark #121317. Type: Space Grotesk (display) / Manrope (body) /
JetBrains Mono (time & meta). Sharp corners (3-4px), 4pt spacing grid.
Dark mode follows the OS.
Never introduce colors, fonts, or radii outside the tokens.
brand-kit/theme/tokens.json— source of truth for all design valuesbrand-kit/guidelines/brand-guidelines.html— visual referenceInterlinedList/Resources/Palette.xaml— brand palette ResourceDictionary (derived from tokens.json)InterlinedList/Resources/Theme.Light.xaml— light semantic brushesInterlinedList/Resources/Theme.Dark.xaml— dark semantic brushes
InterlinedList.slnx
InterlinedList/
InterlinedList.csproj (net10.0-windows, UseWPF=true)
app.manifest (Windows 10+ DPI awareness + UAC)
App.xaml / App.xaml.cs (Entry point: theme switching + login/session orchestration)
LoginWindow.xaml / .xaml.cs (Email/password gate, shown when no session restores)
MainWindow.xaml (Shell: custom title bar, left nav, center ContentControl, right rail)
MainWindow.xaml.cs (Code-behind: clock, nav → view switching, window chrome)
Resources/
Palette.xaml (Invariant brand colors)
Theme.Light.xaml (Light semantic brushes)
Theme.Dark.xaml (Dark semantic brushes)
Models/ (Wire types matching the real API JSON — see API integration below)
Services/
ApiConfig.cs (Base URL: https://interlinedlist.com/)
InterlinedApiClient.cs (Core: HTTP/JSON plumbing + auth/user/messages/dig/notifications)
InterlinedApiClient.Lists.cs (partial class: Lists domain)
InterlinedApiClient.Documents.cs (partial class: Documents domain)
InterlinedApiClient.Organizations.cs (partial class: Organizations domain)
InterlinedApiClient.Search.cs (partial class: cross-resource search)
InterlinedApiClient.CrossPost.cs (partial class: linked-identity/OAuth helpers)
InterlinedApiException.cs
CredentialStore.cs (DPAPI-encrypted sync-token persistence)
SessionService.cs (login/logout/restore, exposes CurrentUser)
AppServices.cs (process-lifetime singletons: Api, Session)
ViewModels/ (CommunityToolkit.Mvvm ObservableObject + [RelayCommand])
LoginViewModel.cs, FeedViewModel.cs, MessageItemViewModel.cs,
NotificationsViewModel.cs, NotificationItemViewModel.cs, ProfileSummaryViewModel.cs,
ListsViewModel.cs, DocumentsViewModel.cs, OrganizationsViewModel.cs,
SearchViewModel.cs, ConnectedAccountsViewModel.cs
Views/ (Self-contained UserControls; each owns its ViewModel —
`DataContext = new XyzViewModel(AppServices.Session)` in its
own constructor, not injected by MainWindow)
FeedView, ListsView, DocumentsView, OrganizationsView, SearchView, ConnectedAccountsView
installer/
InterlinedList.Installer.wixproj (WiX v5 SDK project → classic .msi)
Package.wxs (product/feature/shortcut definition)
License.rtf (placeholder EULA for WixUI_Minimal)
InterlinedList.Package/
InterlinedList.Package.wapproj (Windows Application Packaging Project → MSIX)
Package.appxmanifest (Store identity, visual elements, capabilities)
Images/ (tile/splash assets generated from brand-kit logo)
The app talks to the real InterlinedList backend at https://interlinedlist.com
(154-endpoint REST API, OpenAPI spec at /api/openapi.json) — there is no mock
data layer. Auth is a long-lived bearer token from POST /api/auth/sync-token
(the same mechanism the il-sync CLI and other native clients use — no cookie
jar), persisted DPAPI-encrypted via CredentialStore. The token is
long-lived, so treat %LocalAppData%\InterlinedList\session.dat as a standing
credential — but note the server does expose session management:
GET /api/user/sessions lists a user's active sync tokens and
DELETE /api/user/sessions/{id} revokes one (both accept the bearer token —
verified live 2026-07-31). An earlier revision of this file claimed no revoke
endpoint existed; that is no longer true.
Covered now (greatly expanded in the 2026-07-31 parity build-out):
login/session restore, paginated feed with compose (text + image
attachments, cross-post toggles), Dig/Undig, replies/threads, edit/
delete own posts, report posts, and click-through to author profiles;
notifications tray with mark-one-read / delete-one / mark-all; Direct
Messages (recipient list + thread + send); People (profile lookup,
follow/unfollow, follow-request approve/reject, a user's messages,
block/mute/report); Lists (browse/create/delete, freeform JSON data
rows with row edit + delete — no schema/column editor, see below);
Documents (root docs, templates, create/edit/delete + folder CRUD /
new-doc-in-folder); Organizations (browse + create + full member
management: add via search, change role, remove, edit/delete org);
Settings (profile edit, avatar-from-URL, email change, notification
preferences, blocked/muted management, API-session list + revoke, CSV
data export); unified Search; and Connected Accounts (Bluesky/
Mastodon/LinkedIn/Twitter linking + cross-post toggles). Still not built:
Stripe billing UI, register/forgot-password, GitHub issue sync (endpoints work
but the test account has no GitHub linked), per-list schema/column definitions,
LinkedIn per-page posting targets, scheduled-post UI (the service supports
scheduledAt), media video upload, list watchers/sharing, document sharing/
collaborators, Materialize ("Create from…"), and account deletion UI (the
service method exists, intentionally unsurfaced).
Load-bearing constraints discovered by live-probing the API — don't "fix" these without re-verifying, they're not bugs in this app:
- A few endpoints only accept cookie-session auth, not the bearer
sync-token. Re-probed live 2026-07-31 with the test account:
GET /api/user/engagementandGET/PUT /api/user/dashboard-layoutreturn401with a valid bearer token (Stripe billing + some/api/auth/*session flows are the same shape). A native bearer-token client structurally can't get a cookie session, so those are either browser-handoff (like OAuth) or out of scope. Correction to an earlier claim:GET /api/organizations/{id}/members,GET /api/linkedin/targets, andGET /api/linkedin/posting-targetswere previously documented here as401-walled, but as of 2026-07-31 they return200with the bearer token — member-management and LinkedIn per-page targeting are buildable now. (Member mutations — POST/PUT/DELETE — still need live write-verification.) - The per-provider
GET /api/auth/{provider}/statusendpoints are a red herring — they report whether the server has that OAuth integration configured, not whether this user has linked it. The real per-user link state isGET /api/user/identities(works fine with the bearer token), which is whatConnectedAccountsViewModelactually uses.
Write-path caution, same pattern throughout: PostMessageAsync/
DigAsync/UndigAsync/AddListRowAsync/CreateOrganizationAsync/
RemoveIdentityAsync don't parse their response bodies — some were
deliberately never exercised live (creating an org, disconnecting a linked
identity) to avoid mutating shared test infrastructure, so callers re-fetch
from a GET afterward rather than trusting a typed write response. Lists'
schema/column DSL (PUT /api/lists/{id}/schema) was only partially reverse
engineered and is not implemented — data rows work fine schema-less
(confirmed live), so that's the supported path. If you pick up any of this,
verify the actual response shape against a real (test) account before typing
it strictly, and prefer read-after-write over trusting an unverified envelope.
Left nav maps to real views now: Feed, Messages (Direct Messages),
Lists, Documents, Organizations, People (profiles + follow),
Search (MainWindow.xaml.cs NavItem_Click swaps a ContentControl via a
small per-tag cache in _views), Accounts (Connected Accounts),
Settings, and Alerts (right-rail toggle, not a center view). Feed/search
cards open a profile in the People tab via the Navigator hub
(Services/Navigator.cs) → MainWindow.OpenProfile.
Two independent, parallel packaging tracks — both wrap the same
InterlinedList/InterlinedList.csproj build, neither depends on the other:
MSI (installer/) — WiX Toolset v5 (SDK-style, NuGet-restored via
WixToolset.Sdk). It's a two-step build, not one: publish the app first,
then build the installer —
dotnet publish InterlinedList/InterlinedList.csproj -c Release -r win-x64 --self-contained -p:PublishSingleFile=false
dotnet build installer/InterlinedList.Installer.wixproj -c ReleaseWiX then harvests everything under
InterlinedList/bin/Release/net10.0-windows/win-x64/publish/** into the MSI via
a <Files Include> glob (no manual harvesting/heat step). A single-command
BeforeTargets="Build" auto-publish target was tried and removed — WiX's
file harvesting runs before that hook ever fires, confirmed empirically in CI
(the publish directory was still missing when harvesting ran), so don't
reintroduce that pattern without verifying it actually executes. Produces
InterlinedList-Setup.msi for direct download/side-loading, Start Menu
shortcut, per-machine install under Program Files. Before shipping:
replace installer/License.rtf with the real EULA.
MSIX (InterlinedList.Package/) — classic Desktop Bridge "Windows
Application Packaging Project" (.wapproj), the standard route for putting
an existing Win32/.NET desktop app into the Microsoft Store or sideloaded
MSIX. This project type is not dotnet build-able — its targets come
from Microsoft.DesktopBridge.props/.targets, installed with Visual Studio's
"Universal Windows Platform development" workload, not a NuGet package. It
builds fine headlessly with classic MSBuild once that workload is present
(confirmed in CI — see below); you don't need the VS IDE itself, just its
installed build tools. Command (matches what CI runs):
msbuild InterlinedList.Package/InterlinedList.Package.wapproj /restore `
/p:Configuration=Release /p:Platform=x64 /p:AppxBundlePlatforms=x64 `
/p:AppxBundle=Always /p:UapAppxPackageBuildMode=StoreUploadUapAppxPackageBuildMode=StoreUpload produces an unsigned .msixupload
bundle meant to be uploaded directly to Partner Center — Partner Center signs
it during ingestion, so no code-signing certificate is needed for Store
submission (you would need one for direct sideloading instead, a different
UapAppxPackageBuildMode). Before Store submission: replace the
placeholder Publisher value in Package.appxmanifest with the identity
reserved in Partner Center (VS's "Associate App with the Store" wizard will
rewrite Identity/Properties for you if you do it from the IDE instead).
TargetPlatformVersion must match a UAP SDK actually installed on the
build machine — this isn't a fixed "latest is fine" choice. Different
machines (and different GitHub Actions runner image versions over time) have
different SDKs installed; check what's present rather than assuming
(Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\Platforms\UAP") if a
future SDK-not-found error (APPX3217) shows up after a runner image update.
Image assets in InterlinedList.Package/Images/ were generated from
brand-kit/logo/logo-icon-master.png with transparent padding (tiles) and a
teal-deep #0C2C3A background (splash) — regenerate them the same way if the
mark changes, don't hand-edit the PNGs.
.github/workflows/build.yml runs on every push/PR to main/dev (and via
manual workflow_dispatch), on windows-latest, with three jobs:
build-app— fast sanity-checkdotnet buildof the WPF app alone; the other two jobsneeds:this one so a trivial compile break fails fast instead of waiting on a much slower packaging build.build-msi— publishes the app, then builds the WiX MSI, uploadsInterlinedList-Setup-msias a workflow artifact.build-msix— addsmicrosoft/setup-msbuild(locates VS's MSBuild) then builds the.wapprojdirectly (see above), uploadsInterlinedList-Store-package(the AppxBundle +.msixupload) as an artifact.
Both packaging jobs were debugged against real CI runs, not assumptions — three real, non-obvious issues surfaced and are fixed in the current state (don't reintroduce them):
Package.wxsdeclaredARPNOMODIFYitself, which collides with the same property already set by theWixUI_Minimalwixlib (WIX0091duplicate symbol) — removed.- The app project needs
<RuntimeIdentifiers>win-x64</RuntimeIdentifiers>declared (not just passed via-r win-x64on the CLI) — the MSIX packaging project triggers a nested publish of it as aProjectReferencethat needs the RID available at restore time (NETSDK1047otherwise). TargetPlatformVersionin the.wapprojmust match an SDK actually installed on the runner (see above) — it drifts as GitHub updates runner images, so a future image update could reintroduce this failure.
Releases — .github/workflows/release.yml triggers on a pushed v* tag
(e.g. git tag v1.0.0 && git push origin v1.0.0). It runs the same
publish → WiX MSI build as CI, then attaches InterlinedList-Setup.msi to a
GitHub Release for that tag (auto-generated notes). Keep the tag version in
sync with installer/Package.wxs Version and Package.appxmanifest
Version (both 1.0.0.0 today) — bump all three together for a new release.
- App icon:
brand-kit/icons/windows/InterlinedList.ico(set via ApplicationIcon in .csproj) - Title bar:
WindowChrome(custom chrome, native resize); deep-teal#0C2C3A - Window controls: right-aligned min / max / close; close highlights red on hover
- Card corners: 4px (
CornerRadius="4") - Post card left edge: 4px wide; teal by default, amber once you've Dug that message (there's no server-side "stream type" to color by — see API integration)
- Dark mode: read from HKCU registry at launch + listen via
SystemEvents.UserPreferenceChanged
EnableWindowsTargeting is set to true in the csproj to allow dotnet build
cross-compilation checks. The output still targets Windows only and must be run
on a Windows 10+ machine or VM.
Open InterlinedList.slnx in Visual Studio 2022 and press F5, or:
dotnet build InterlinedList/InterlinedList.csproj -r win-x64
dotnet run --project InterlinedList/InterlinedList.csproj -r win-x64