Search by

grey-ooo / brand-kit

matthewbaggett

Shared grey.ooo brand kit: a consistent top bar, app switcher and app shell for Grey.ooo Symfony apps.

Package info

git.grey.ooo/Grey.ooo/symfony-brand-kit/

Type:symfony-bundle

pkg:composer/grey-ooo/brand-kit

Statistics

Installs: 200

Dependents: 0

Suggesters: 0


README

grey-ooo/brand-kit — a small Symfony bundle that ships the shared grey.ooo chrome: a consistent top bar, an app switcher, and an app shell (top bar + left sidebar + centered main + footer). Drop it into any Grey.ooo Symfony app so they all wear the same skin.

The chrome is self-contained: all styles are namespaced under .gooo-* and driven by --gooo-* custom properties, so it renders identically whatever CSS/Tailwind setup the host app uses, and needs no build step. It is dark-first, matching the someones.computer aesthetic.

Install

composer require grey-ooo/brand-kit

Register the bundle (config/bundles.php):

GreyOoo\BrandKit\GreyOooBrandKitBundle::class => ['all' => true],

Configure it (config/packages/grey_ooo_brand_kit.yaml) — normally just the one line naming which app this is:

grey_ooo_brand_kit:
    current_app: printshop

The app roster is owned by the bundle, not by consumers: it defaults to GreyOoo\BrandKit\Config\GreyOooApps::ALL, rather than the same list repeated in every repo where the copies drift.

That constant is generated, not hand-edited. An app is in the roster because its own repo on git.grey.ooo carries the grey-ooo-app-roster topic, and its URL is that repo's website field; a nightly job regenerates the constant and opens the PR. Adding an app is a topic on its repo plus a name and icon in tools/roster/presentation.mjs, then a composer update grey-ooo/brand-kit in each consumer. See tools/roster/README.md.

You do not set external per app either. Whether an entry is "another app" depends on who is rendering, so it is derived from current_app: every entry is external except the one being served. Set it explicitly only to force a full page load.

Each entry carries an icon, in one of two forms — the switcher, the brand mark and the styleguide all render it through components/_app_icon.html.twig, so they agree:

Field
iconAn emoji. The default, and enough for an app with no mark of its ownescaped
icon_svgInline SVG, superseding icon for an app that has a mark — someones ships the terminal display it draws in its own appbar, niall the face he talks withprinted raw

Give icon_svg geometry only (a viewBox, no width/height): brand-kit.css sizes it in em so a mark and an emoji sit at one optical scale side by side. It is the only field the kit prints unescaped, so it takes bundle or app config and nothing a request can reach.

An entry may also carry staging_url, which gives its row a blade: a second, amber surface behind the button showing a sliver past the right edge, sliding out from under it on hover (and simply sitting out on touch, where a 4px sliver is not a tap target). It reads as one app with another copy behind it rather than as two apps, which is why it is drawn behind the row and not beside it. The blade lives in the shared roster, so it shows in every app's switcher — a staging box linked here is a shortcut, not a hiding place.

The signed-in user

The right side of the top bar shows who signed in at the Authelia portal. A chip with the name opens a menu with the email, Manage account, and Sign out. With nobody signed in, the bar shows a Sign in button instead. Both go to sso.grey.ooo and come back to the page the visitor left.

The chip with its menu open The Sign in button

An app chooses what to show, in config or for one page:

ValueResult
true (default)The chip when someone signed in. The Sign in button when nobody did
'user-only'The chip when someone signed in. Nothing when nobody did
falseNothing
grey_ooo_brand_kit:
    auth:
        enabled: user-only
        sso_url: https://sso.grey.ooo   # the default
<twig:GreyOoo:TopBar current-app="printshop" :show-auth="false" />

The page value wins over the config. Pass a real boolean (:show-auth="false"). A quoted "false" also reads as false.

The bundle reads the name from the Remote-Name, Remote-Email and Remote-User headers. Authelia sets them, and the reverse proxy passes them on. Anyone can send a header, so the bundle trusts them only when the request came from a trusted proxy. Set framework.trusted_proxies in the app. With none set, the bar never shows a name from headers and shows Sign in. An app with its own sign-in can skip the headers and place <twig:GreyOoo:UserChip name="…" email="…" />.

The injected bar (assets/bar.js) asks the portal. It calls /api/user/info on sso.grey.ooo with the session cookie. The portal must send Cross-Origin Resource Sharing (CORS) headers for the injected hosts. See Infrastructure#503. Until that lands, the injected bar shows Sign in. Add data-gooo-auth="off" to the injecting <script> tag to hide the user chip.

The menu links use .gooo-user__link, not .gooo-menu__item. The conformance checker compares each .gooo-menu__item with the roster.

Override the list only when you genuinely need a different one (staging URLs, a local sandbox); apps still accepts a full replacement, and brand still tunes the wordmark:

grey_ooo_brand_kit:
    current_app: printshop
    brand: { name: grey, suffix: .ooo }
    apps:
        - { key: printshop, name: 'PrintShop (staging)', url: 'https://staging.printshop.grey.ooo', icon: '🖨️' }

Expose the bundle's assets to AssetMapper (config/packages/asset_mapper.yaml):

framework:
    asset_mapper:
        paths:
            '%kernel.project_dir%/vendor/grey-ooo/brand-kit/assets': grey-ooo-brand-kit

Then load the CSS + JS. Link the stylesheet in your base template <head> and pull the script in via your importmap.php / app.js (or a <script type="module">):

<link rel="stylesheet" href="{{ asset('grey-ooo-brand-kit/styles/brand-kit.css') }}">

The JS drives the app-switcher dropdown and the mobile sidebar drawer — without it both render but never open. With AssetMapper, add it to importmap.php and import it for its side effects:

// importmap.php
'grey-ooo-brand-kit/gooo-brand-kit.js' => ['path' => 'grey-ooo-brand-kit/gooo-brand-kit.js'],
// assets/app.js
import 'grey-ooo-brand-kit/gooo-brand-kit.js';

Use

<twig:GreyOoo:Shell current-app="printshop">
    <twig:block name="topbar">
        <twig:GreyOoo:TopBar current-app="printshop">
            {# host app actions: notifications, user menu, … #}
        </twig:GreyOoo:TopBar>
    </twig:block>

    <twig:block name="sidebar">
        <twig:GreyOoo:NavItem href="{{ path('dashboard') }}" label="Dashboard" :active="true">
            <twig:block name="icon"><twig:ux:icon name="lucide:layout-dashboard"/></twig:block>
        </twig:GreyOoo:NavItem>
        {# … #}
    </twig:block>

    <twig:block name="content">{# page content #}</twig:block>
    <twig:block name="footer">© grey.ooo</twig:block>
</twig:GreyOoo:Shell>

Fill slots with <twig:block name="…">, never {% block … %}. The component pre-lexer copies a traditional {% block %} body out verbatim, so a <twig:…> tag nested inside one is never parsed — it is sent to the browser as literal tag text and the component silently never renders. Only <twig:block> bodies get sub-lexed. The two forms look interchangeable and the Twig linter passes either way, so this fails without a single error: the page just quietly loses its top bar.

Icons are passed via the icon block so the bundle stays independent of any icon set.

The sidebar is optional: omit the sidebar block and the shell renders without a rail and without the drawer toggle, with main spanning the full width. An app with a single page (Niall.chat) has nothing to navigate to, so it leaves the block out.

Components

ComponentPurpose
GreyOoo:ShellPage scaffold — topbar / sidebar (optional) / content / footer blocks.
GreyOoo:TopBarThe grey.ooo bar: mark + wordmark + app switcher, with a right-hand actions slot (default content).
GreyOoo:AppSwitcherGrid-button dropdown of the configured apps.
GreyOoo:NavItemA sidebar link (href, label, active, icon block).

The resolved config is also available in templates as the gooo_brand_kit global.

Style guide

Import the bundle's routes (dev only) to browse a living style guide at /_brand-kit/styleguide:

# config/routes/grey_ooo_brand_kit.yaml
grey_ooo_brand_kit:
    resource: '@GreyOooBrandKitBundle/config/routes.php'