Skip to content

feat(platform): add branding and UI customization settings#493

Merged
Israeltheminer merged 4 commits into
mainfrom
feat/branding-customization
Feb 19, 2026
Merged

feat(platform): add branding and UI customization settings#493
Israeltheminer merged 4 commits into
mainfrom
feat/branding-customization

Conversation

@Israeltheminer

@Israeltheminer Israeltheminer commented Feb 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add a Settings > Branding page (admin-only) for white-labeling the platform with custom app name, text logo, logo image, favicon (light/dark), brand color, and accent color
  • Live preview via a browser-chrome mockup updates in real-time as form values change
  • Runtime branding provider applies custom logos, favicons, and document title across the app
  • New brandingSettings Convex table with query, mutation (including storage cleanup and audit logging)
  • Shared Zod validation schema between client and server

What's included

Data layer: brandingSettings table, getBranding query, upsertBranding mutation (admin-only, audit logged)

UI components: BrandingSettingsClient (two-column layout), BrandingForm (react-hook-form + Zod), BrandingPreview (live mockup), ColorPickerInput, ImageUploadField

Runtime branding: BrandingProvider context, custom logo rendering in TaleLogo/TaleLogoText, dynamic favicon and page title

Tests: 43 tests across 5 test files (queries, mutations, color picker, preview, form)

Stories: ColorPickerInput (5 stories), ImageUploadField (5 stories)

Closes #323

Test plan

  • Navigate to Settings > Branding tab (visible only to admins)
  • Fill in app name, text logo, upload logo/favicons, set colors
  • Verify live preview updates in real-time
  • Save and reload — verify values persist
  • Verify custom logo renders in sidebar
  • Verify custom favicon and title in browser tab
  • Remove all customizations — verify Tale defaults restore
  • Run npm run test --workspace=@tale/platform (487 tests passing)
  • Run npm run lint --workspace=@tale/platform (0 errors)

Summary by CodeRabbit

  • New Features
    • Added branding customization for organizations. Admins can now configure app name, text logo, logo image, favicon files, brand color, and accent color.
    • New branding settings page accessible through the organization settings dashboard.
    • Real-time preview feature showing how branding customizations appear throughout the application while editing.

Add a Settings > Branding page where admins can customize the platform's
appearance with custom app name, logos, favicons, and brand/accent colors.
Changes are previewed in real-time via a browser-chrome mockup before saving.
@greptile-apps

greptile-apps Bot commented Feb 18, 2026

Copy link
Copy Markdown

Greptile Summary

Adds a full-stack branding/white-labeling feature: a new Settings > Branding admin page with a form for customizing app name, text logo, logo image, favicons, and brand/accent colors, plus a live browser-chrome mockup preview. The backend introduces a brandingSettings Convex table with query and admin-only mutation (including storage cleanup and audit logging), while a BrandingProvider context applies custom logos, favicons, and document title at runtime across the dashboard.

  • Data layer: New brandingSettings table with by_organizationId index, getBranding query (resolves storage URLs), upsertBranding mutation (admin-only, cleans up replaced storage files, audit logged)
  • UI: BrandingForm (react-hook-form + Zod), BrandingPreview (live mockup), ColorPickerInput, ImageUploadField, admin-only route and nav entry
  • Runtime branding: BrandingProvider wraps the dashboard layout, TaleLogo/TaleLogoText conditionally render custom logos
  • Tests & stories: 43 tests across 5 files, 10 Storybook stories across 2 components
  • Issues found: Object URL memory leak in ImageUploadField (cleanup function returned from event handler is never invoked), faviconDarkUrl is fetched but never applied in the favicon effect, and size variant classes appear inverted in ImageUploadField

Confidence Score: 3/5

  • Generally well-structured feature with a few bugs in the image upload component and an incomplete dark-mode favicon implementation that should be fixed before merging.
  • The overall architecture is solid and follows established codebase patterns (RLS, audit logging, settings route structure). However, three issues lower confidence: (1) the ImageUploadField has an object URL memory leak where the cleanup function is returned from an event handler instead of a useEffect, (2) the size variant classes are inverted making sm larger than md, and (3) the BrandingProvider fetches faviconDarkUrl but only applies faviconLightUrl, leaving dark-mode favicon support incomplete.
  • services/platform/app/features/settings/branding/components/image-upload-field.tsx (memory leak + inverted size classes), services/platform/app/components/branding/branding-provider.tsx (dark favicon not applied)

Important Files Changed

Filename Overview
services/platform/app/components/branding/branding-provider.tsx New branding context provider with title/favicon runtime overrides. Dark favicon URL is fetched but never applied — only faviconLightUrl is used in the favicon effect.
services/platform/app/features/settings/branding/components/branding-form.tsx React Hook Form with Zod validation for branding settings. Uses useCallback-based refs for storage IDs (outside RHF). Solid form logic with proper dirty/submit state handling.
services/platform/app/features/settings/branding/components/branding-preview.tsx Memoized browser-chrome mockup preview component. Renders live brand/accent color styling and conditional logo display. Well-structured presentational component.
services/platform/app/features/settings/branding/components/image-upload-field.tsx Image upload component with Convex storage integration. Has two issues: object URL memory leak (cleanup returned from event handler, never invoked) and inverted size variant classes (sm renders larger than md).
services/platform/convex/branding/mutations.ts Upsert mutation with ADMIN_ONLY RLS, storage cleanup on replacement, and audit logging. Color fields lack server-side hex validation (client-only Zod regex).
services/platform/convex/branding/queries.ts Query resolves storage IDs to URLs via Promise.all. Properly returns null when no branding exists. Correct pattern.
services/platform/convex/branding/schema.ts Schema definition with appropriate fields and by_organizationId index. All image fields use v.id('_storage') type.
services/platform/lib/shared/schemas/branding.ts Shared Zod validation schema with hex color regex, form data type, settings type with storage IDs, and URL-resolved type. Proper schema composition.
services/platform/app/routes/dashboard/$id.tsx Wraps dashboard layout in BrandingProvider as outermost context. Clean integration with existing provider hierarchy.

Sequence Diagram

sequenceDiagram
    participant Admin as Admin User
    participant Form as BrandingForm
    participant Upload as ImageUploadField
    participant Convex as Convex Backend
    participant Storage as Convex Storage
    participant Provider as BrandingProvider
    participant Logo as TaleLogo / TaleLogoText

    Admin->>Form: Fill in branding fields
    Form->>Form: Live preview update via onPreviewChange

    Admin->>Upload: Select image file
    Upload->>Convex: generateUploadUrl()
    Convex-->>Upload: uploadUrl
    Upload->>Storage: POST file to uploadUrl
    Storage-->>Upload: storageId
    Upload->>Form: onUpload(storageId)

    Admin->>Form: Click Save
    Form->>Convex: upsertBranding(orgId, fields, storageIds)
    Convex->>Convex: validateOrganizationAccess(ADMIN_ONLY)
    Convex->>Storage: delete(oldStorageId) if replaced
    Convex->>Convex: db.patch() or db.insert()
    Convex->>Convex: AuditLog.logSuccess()
    Convex-->>Form: null (success)
    Form->>Admin: Toast "Branding updated"

    Note over Provider: On next render / reactivity update
    Provider->>Convex: getBranding(orgId)
    Convex->>Storage: getUrl(storageIds)
    Convex-->>Provider: { appName, logoUrl, faviconUrls, colors }
    Provider->>Provider: Update document.title suffix
    Provider->>Provider: Update favicon link element
    Provider->>Logo: Context provides logoUrl / textLogo
    Logo->>Logo: Render custom logo or default
Loading

Last reviewed commit: b90c32e

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

28 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment thread services/platform/app/components/branding/branding-provider.tsx Outdated
Comment thread services/platform/convex/branding/mutations.ts
@coderabbitai

coderabbitai Bot commented Feb 18, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR introduces a complete branding customization system enabling users to configure their organization's app name, logo, text logo, favicons, and colors. Changes span the backend (Convex schema, queries, mutations for branding data persistence), frontend (BrandingProvider context, form and preview components for UI configuration), client hooks for data fetching and mutations, a new settings route at /dashboard/$id/settings/branding, file upload handling via ImageUploadField component, color picker inputs, validation schemas, comprehensive test coverage, and localization strings. The feature integrates with existing admin access controls and provides live preview during configuration.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(platform): add branding and UI customization settings' clearly and concisely summarizes the main change—adding branding and UI customization features to the platform.
Linked Issues check ✅ Passed The PR fully implements all objectives from issue #323: custom logo, text logo, favicon, app name, brand/accent colors, admin-only settings UI with live preview, default Tale branding fallback, and storage cleanup with audit logging.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing branding customization as specified in issue #323. No unrelated functionality, refactoring, or out-of-scope modifications were introduced.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/branding-customization

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@services/platform/app/components/branding/branding-provider.tsx`:
- Around line 67-86: The effect captures originalHref each run which can be a
previously set custom favicon; change this to store the initial default href
once and reuse it on cleanup: create a ref (e.g., initialDefaultHrefRef) outside
the effect, on first mount (or when there is no value) set
initialDefaultHrefRef.current = link.href before any modifications in the effect
that uses faviconUrl, update link.href to branding?.faviconLightUrl as now, and
in the returned cleanup restore link.href = initialDefaultHrefRef.current
(falling back to empty string if unset); update the effect that references
branding?.faviconLightUrl and use the ref names initialDefaultHrefRef and
originalHref only for local temporary reference inside the effect as needed.
- Around line 41-64: The effect currently only handles applying a custom suffix
when branding?.appName is set and returns early when it's cleared, leaving the
modified document.title in place; fix by capturing the original document.title
(or originalSuffix) at the start of the effect and using it to restore the title
when branding?.appName becomes falsy. Concretely: in the useEffect that contains
originalSuffix and updateTitle, read and store const originalFullTitle =
document.title (or derive originalSuffix) before doing any early returns, always
create the MutationObserver and apply updateTitle when branding?.appName is
present, and in the cleanup (or when branding?.appName is undefined) set
document.title = originalFullTitle to restore the original title, then
disconnect the observer; keep references to observer, titleElement, and
originalFullTitle within the effect so updateTitle and cleanup can use them.

In
`@services/platform/app/features/settings/branding/components/__tests__/branding-preview.test.tsx`:
- Around line 84-90: The test currently asserts firstIcon.style.color ===
'#00AAFF' but browsers return computed RGB strings; update the assertion to use
getComputedStyle(firstIcon).color and compare to the equivalent RGB string (e.g.
'rgb(0, 170, 255)') or use a regex that matches the RGB components; modify the
failing test in branding-preview.test.tsx (variables navIcons and firstIcon) to
call getComputedStyle(firstIcon).color and assert that value instead of the hex
string so the assertion is color-format agnostic.
- Around line 92-98: The test "renders browser chrome dots" is using the broad
selector container.querySelectorAll('.rounded-full') which matches avatars too;
update the test to scope the query to the browser chrome container in the
BrandingPreview output (e.g., locate the browser chrome element by its unique
container class or data-testid and call querySelectorAll('.rounded-full') on
that node, or use a more specific selector like '.browser-chrome .rounded-full'
or getByTestId('browser-chrome') then within it query for '.rounded-full') so
only the 3 chrome dots are selected.

In
`@services/platform/app/features/settings/branding/components/branding-form.tsx`:
- Around line 70-108: The three storage-id "refs" (logoStorageIdRef,
faviconLightStorageIdRef, faviconDarkStorageIdRef) are recreated each render
because they use useCallback(() => { ... }, [])(), so stored IDs are lost;
replace these with stable refs using React's useRef (or useState) so the object
persists across renders (e.g., create const logoStorageIdRef =
useRef<Id<'_storage'>|undefined>(undefined) and similarly for
faviconLightStorageIdRef and faviconDarkStorageIdRef) and update the get/set
usage to read/write .current to ensure uploaded IDs survive re-renders and are
included on submit.

In
`@services/platform/app/features/settings/branding/components/image-upload-field.tsx`:
- Around line 46-77: The handleFileChange flow creates object URLs via
URL.createObjectURL and never revokes them, causing memory leaks; add a
useEffect that watches the previewUrl state (the value set by setPreviewUrl in
handleFileChange) and in its cleanup revoke the current previewUrl with
URL.revokeObjectURL(previewUrl) (also ensure you check previewUrl is non-null),
so previous object URLs are revoked on every change and on unmount; keep
existing logic that clears previewUrl on error/finally and reset
fileInputRef.current.value as before.
- Around line 40-125: The remove button only clears previewUrl so displayUrl
still falls back to currentUrl; add a local removed flag (e.g., isRemoved via
useState) and change displayUrl to: previewUrl ?? (isRemoved ? null :
currentUrl). In handleRemove set isRemoved(true) and setPreviewUrl(null) and
call onRemove?.(); when a new file is selected (in handleFileChange) clear
isRemoved (setIsRemoved(false)); also reset isRemoved to false when currentUrl
changes (useEffect watching currentUrl) so updates restore the image. Use the
existing symbols displayUrl, previewUrl, currentUrl, handleRemove,
handleFileChange and onRemove to locate where to add this logic.

Comment thread services/platform/app/components/branding/branding-provider.tsx
Comment thread services/platform/app/components/branding/branding-provider.tsx Outdated
Comment thread services/platform/app/features/settings/branding/components/branding-form.tsx Outdated
- Fix object URL memory leak in image-upload-field by tracking URLs
  in a ref and revoking on replacement, error, and unmount
- Fix inverted size variants (sm was 48px, md was 40px)
- Fix remove button not clearing image when currentUrl is set by
  adding isRemoved state to override the fallback
- Fix document title not restoring to "Tale" when appName is cleared
- Fix favicon cleanup capturing wrong originalHref on subsequent runs
  by using a ref to store the true default once
- Add dark mode favicon support using matchMedia listener
- Add server-side hex color validation in upsertBrandingHandler
- Replace useCallback closure anti-pattern with idiomatic useRef for
  storage ID tracking in branding-form
- Fix test assertions: use toHaveStyle for color checks, scope
  browser chrome dots query via data-testid
Israeltheminer and others added 2 commits February 19, 2026 06:52
Use oxfmt formatting and remove unused brandingSettingsSchema,
BrandingSettings, brandingWithUrlsSchema, BrandingWithUrls exports
to fix CI lint/format and knip failures.
@Israeltheminer Israeltheminer merged commit c697791 into main Feb 19, 2026
17 checks passed
@Israeltheminer Israeltheminer deleted the feat/branding-customization branch February 19, 2026 05:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Branding and UI Customization

1 participant