@salla.sa/twilight-theme-engine
v1.0.33
Published
Core engine for Salla React themes - component registry, hooks, contexts, routes, and base components
Readme
@salla.sa/twilight-theme-engine
The SDK behind Salla React themes. It ships the storefront routes, components, hooks, API clients, and the Vite plugin that a theme composes — so a theme is a small app, not a fork of a storefront.
Built for TanStack Start, server-rendered at the edge.
Installation
You normally don't install this yourself — salla theme create --type react scaffolds a theme
that already depends on it.
pnpm add @salla.sa/twilight-theme-enginePeer dependencies are pinned to tilde ranges (~1.166.2 for @tanstack/*, vite ^8.1.5,
react ^19.2.4). The engine's dist references @tanstack/react-start internals, so drifting
outside the tested minor breaks SSR at runtime. Let the install warn you rather than forcing a
resolution.
What's in it
| Area | What it gives you |
| -------------- | ---------------------------------------------------------------------------- |
| Routes | Ready-made storefront pages — product, cart, category, blog, /account/*, … |
| Components | Header/Footer, product, cart, home blocks, modal/drawer/dropdown primitives |
| Hooks | useStore, useTheme, useUser, useMoney, useAsset, useWishlist, … |
| Registry | Swap any registered component by name |
| Hook slots | Inject content at defined extension points |
| API | Typed clients for the Salla store API, for use in SSR loaders |
| i18n | react-i18next integration, Arabic/RTL first |
| Tooling | Vite plugin (route generation, virtual modules), ESLint config, SSR runtime |
Documentation
Theme-facing guides live in docs/ at the repo root — start with the
Getting Started track or the
Architecture Overview.
Engine-internal deep dives are in docs/ here:
| Document | Topic |
| --------------------------------------------------------------- | -------------------------------------------------- |
| HOME_COMPONENTS.md | Home component system and the data prop contract |
| usePageConfig.md | Unified page configuration |
| useLocation.md | Location and query-param handling |
| store-identity.md | Which store a request is for, and why not config |
| router-context.md | Request-scoped context across SSR and CSR |
| route-ids.md | Route identifiers |
| route-pagination.md | Pagination in loaders |
| ProductCard.md | The ProductCard contract |
| gtm-integration.md | Google Tag Manager |
| i18n-instance-lifecycle.md | How the i18n instance is created and shared |
| type-improvement-guide.md | Typing conventions |
Subsystem notes: src/api/README.md,
src/vite/README.md, tests/README.md.
Usage
Wiring the engine into a theme
Two files do it. app/start.ts installs the request middleware:
import { createStart } from '@tanstack/react-start';
import { twilightMiddleware, earlyHintsMiddleware } from '@salla.sa/twilight-theme-engine/tanstack';
export const startInstance = createStart(() => {
return {
requestMiddleware: [twilightMiddleware(), earlyHintsMiddleware()],
};
});app/routes/__root.tsx builds the root route and renders the provider:
import { TwilightProvider } from '@salla.sa/twilight-theme-engine';
import { createTwilightRootRoute } from '@salla.sa/twilight-theme-engine/tanstack';
import themeTranslations from 'virtual:twilight/theme-translations';
export const Route = createTwilightRootRoute()({ shellComponent: RootComponent });<TwilightProvider> takes the bundled translations (plus optional debug, skeleton,
onReady, onError). Store, locale, and user are not props — the middleware resolves them
per request.
Component registry
import { registry, Component } from '@salla.sa/twilight-theme-engine';
registry.register('product:card', ProductCard);
registry.override('product:card', MyProductCard);
<Component name="product:card" product={product} />;Keys are colon-separated, and the engine only resolves three of them: product:card, the
home:* blocks (via registerHomeComponents()), and account:layout-pending. Overriding any
other name is a silent no-op — everything else is imported directly.
Hook slots
import { defineHooks } from '@salla.sa/twilight-theme-engine/hooks/HookRegistry';
import { HookSlot } from '@salla.sa/twilight-theme-engine/hooks/HookSlot';
defineHooks({
'body:start': () => <AnnouncementBar />,
'product:form.end': ({ product }) => <TrustBadges product={product} />,
});
<HookSlot name="product:form.end" context={{ product }} />;Store, theme, and user
import { useStore } from '@salla.sa/twilight-theme-engine/hooks/useStore';
import { useTheme } from '@salla.sa/twilight-theme-engine/hooks/useTheme';
import { useTwilight } from '@salla.sa/twilight-theme-engine';
function MyComponent() {
const store = useStore();
const { color, font, settings, isRTL } = useTheme();
const { user } = useTwilight();
const isDark = settings?.footer_is_dark ?? false;
}Settings are a plain object — read them with
?.and your own default. There is nosettings.get().
Translations
import { useTranslation } from '@salla.sa/twilight-theme-engine/i18n';
function MyComponent() {
const { t, locale, isRTL } = useTranslation();
return (
<>
<h1>{t('pages.cart.title')}</h1>
<p>{t('pages.products.sold_times', { count: 5 })}</p>
</>
);
}useTranslation() returns react-i18next's t, i18n, and ready, plus locale info
(locale, direction, isRTL, isLTR, languageName). The /i18n subpath also exports
I18nProvider, FALLBACK_LOCALE, SUPPORTED_LOCALES, RTL_LOCALES, isLocale, and
getLanguageInfo.
Money and assets
import { useMoney } from '@salla.sa/twilight-theme-engine/hooks/useMoney';
import { useAsset } from '@salla.sa/twilight-theme-engine/hooks/useAsset';
function ProductPrice({ product }) {
const { format } = useMoney(); // also: parse, isValid
const { cdn } = useAsset(); // also: asset, isPlaceholder
return (
<div>
<img src={cdn(product.image.url, 400)} alt="" loading="lazy" />
<p>{format(product.price)}</p>
</div>
);
}format() returns a ReactNode (not a string) — it renders the currency symbol as markup.
Import paths
The engine has ~90 subpath exports. Import from the narrowest one so bundles stay small:
// Core — provider, registry, context
import {
TwilightProvider,
useTwilight,
registry,
Component,
} from '@salla.sa/twilight-theme-engine';
// Hooks — one subpath each
import { useStore } from '@salla.sa/twilight-theme-engine/hooks/useStore';
import { useMoney } from '@salla.sa/twilight-theme-engine/hooks/useMoney';
// i18n
import { useTranslation, I18nProvider } from '@salla.sa/twilight-theme-engine/i18n';
// Components — grouped by domain
import { Link } from '@salla.sa/twilight-theme-engine/common';
import { Header, Footer } from '@salla.sa/twilight-theme-engine/layout';
import { CartItem, CartSummary } from '@salla.sa/twilight-theme-engine/cart';
import { ProductCard } from '@salla.sa/twilight-theme-engine/product';
// Routes
import { Home, DefaultHomeComponents } from '@salla.sa/twilight-theme-engine/routes/home';
// API clients — for loaders
import { store } from '@salla.sa/twilight-theme-engine/api/store';
// TanStack integration
import { createRouter, createTwilightRootRoute } from '@salla.sa/twilight-theme-engine/tanstack';
// Types
import type { Store, Theme, Product } from '@salla.sa/twilight-theme-engine/types';The authoritative list is the exports field of package.json. Never deep-import
into dist/ — those paths aren't part of the public API.
Development
From the repo root:
pnpm --filter @salla.sa/twilight-theme-engine build # tsup + tsc + dts stubs
pnpm --filter @salla.sa/twilight-theme-engine test
pnpm --filter @salla.sa/twilight-theme-engine test:coverage
pnpm --filter @salla.sa/twilight-theme-engine typecheck| Command | Description |
| ---------------- | ---------------------------------------------------------------- |
| pnpm build | Bundle, add JS extensions, emit declarations and ambient .d.ts |
| pnpm dev | tsup --watch |
| pnpm test | Vitest (test:watch, test:coverage, test:ci) |
| pnpm typecheck | tsc --noEmit |
| pnpm clean | Remove dist/ |
The themes consume dist/, so build the engine before running or type-checking a theme.
Adding a new public export means adding a subpath to exports — otherwise consumers can't
reach it. Verify a change survives publishing by installing Tania standalone
(why).
Testing conventions, including the SSR-vs-browser split, are in tests/README.md.
License
MIT
