Skip to content

Commit 7a37823

Browse files
fix(app-shell, plugin-gantt): launcher tile draws app branding; gantt name column names its object (#28017)
domain:ui#3 · os-bill · session_01Utp6CaJoinvGQSbHeDaWBe <!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg8wLwMKzX6awY8FEtvzGD9X)_ Fixes #27386 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:控制台首页的应用图标(及 `app:launcher` 区块)改为读应用自己的品牌:有 `branding.logo` 画 logo,否则有十六进制 `branding.primaryColor` 就用该色的图标块,两者都没有才回到按名字哈希的渐变色;甘特图左侧名称列的表头,绑定对象时改为该对象的标签(如 “Campaign”),不再固定为 “Task Name”。 - **为什么改**:HotCRM 品牌是橙色,首页图标却是粉色(顶栏和应用切换器早已读品牌色,只有首页没读);活动、合同甘特图的名称列都写着 “Task Name”。卡片第 2、4 行,均为纯 ui 改动,不加新键。 - **风险与回滚**:低。未声明品牌的应用外观不变;内联数据、无标签对象、混合对象树(`objectField`)的甘特表头仍是 “Task Name”。回滚即 revert 本 PR。 - **先看**:`ui/packages/app-shell/src/console/home/HomeAppsStrip.tsx` 的 `LauncherAppIcon`;`ui/packages/plugin-gantt/src/ObjectGantt.tsx` 的 `taskColumnLabel` 传参;`ui/packages/app-shell/src/layout/appTileColors.ts`(从 `AppMark` 移来的共享类名与样式)。 - **两分钟验证**:`pnpm --dir ui exec vitest run packages/app-shell/src/console/home/__tests__/HomeAppsStrip.branding-27386.test.tsx packages/plugin-gantt/src/ObjectGantt.taskColumnLabel-27386.test.tsx`,预期 7 个测试全绿。 - **需要你做的**:无,席位落地。 ## What changes Rows 2 and 4 of #27386. Rows 1, 3 and 5 each need a new spec key and are carded by the seat in `domain:spec`: row 1 as #28018 (kanban lanes) and #28020 (view filter OR group), row 3 as #28021 (calendar initial date), row 5 as #28022 (per-audience landing). With every row owned, this PR closes the card. - **Row 4 — launcher tile branding (`@object-ui/app-shell`).** The home "Your apps" strip, which the `app:launcher` block also mounts, drew every app's 56 px icon square on a gradient picked by hashing `app.name`. It now follows the same ladder `AppMark` draws in the top-bar pill and the app switcher: the `branding.logo` image when declared; else the app's `icon` on its `branding.primaryColor` when that is a hex; else the `icon` on the name-keyed gradient, unchanged. A logo that fails to load falls back to the tile. The favourite badge rides on every face. The branded tile reuses `appTileColors` (light and dark colours), and the tile class string plus its CSS custom properties move from `AppMark.tsx` into `appTileColors.ts` (`BRANDED_TILE_CLASSES`, `appTileStyle`) so both surfaces draw one tile. The colour reaches the DOM only as CSS custom properties that static utilities read (the ui/AGENTS.md section 2 author-declared colour carve-out), as `AppMark` already did. - **Row 2 — gantt name column title (`@object-ui/plugin-gantt`).** `GanttView` takes an optional `taskColumnLabel` prop and falls back to the `gantt.column.taskName` string. `ObjectGantt` passes the bound object's label, resolved through `useObjectLabel().objectLabel` (the translated object label every other surface uses). It passes nothing, so the heading stays "Task Name", for an inline data set, an object that declares no label, and a mixed-object tree (`gantt.objectField`), whose rows are not all the bound object. No new key: a configurable title is a spec change and stays out (the card's suggestion of a title key on gantt config is not built). ## Tests - `ui/packages/app-shell/src/console/home/__tests__/HomeAppsStrip.branding-27386.test.tsx` (3 cases): hex colour replaces the gradient and publishes the four custom properties `appTileColors` derives; no colour and a non-hex colour keep the name-keyed gradient; a logo draws, keeps the favourite badge, and falls back to the coloured tile on `error`. - `ui/packages/plugin-gantt/src/ObjectGantt.taskColumnLabel-27386.test.tsx` (4 cases): bound object label heads the column; generic heading for an unlabelled object, a mixed-object tree and an inline data set. The generic cases read the heading after the object schema read has settled. ## Acceptance notes Per-row triage, re-measured on `main` 4694365: | Row | Bucket | Reading | |---|---|---| | 1 | CARD (two) | `KanbanConfigSchema` declares `groupByField`, `summarizeField`, `titleField`, `columns` only: no lane list and no hide-empty key. A list view's `filter` is an array of flat `ViewFilterRuleSchema` rules (`field`, `operator`, `value`), AND-only; the `$or` grouping exists only in `FilterConditionSchema`, which a stored view's `filter` does not take. Both need spec keys. | | 2 | TAKEN (this PR) | Default heading = bound object label; no new key. | | 3 | CARD | `CalendarConfigSchema` declares five field bindings and no initial-date key; `ObjectCalendar` seeds its current date with `new Date()`. #27895 added Studio settings for calendar and gantt but no such key exists in the spec. | | 4 | TAKEN (this PR) | Tile reads `branding.logo` / hex `branding.primaryColor`. | | 5 | CARD | The landing is the first shown nav entry; `homePageId` is retired; `navigationContributions` append only. A per-audience landing is a new app or manifest key. | - The `tile-grid` page block still tints its generic tiles with `launcherIconGradient(href || label)`: those tiles are authored links, not apps, and carry no branding. Not changed. - `AppCard.tsx` (the older marketing card) is not mounted by Home or the launcher block; not changed. ## Verification <details><summary>Ablation (red-first), on 98f7880</summary> Through `scripts/ablation-replace.mjs`, each leg restored with blob equal to HEAD and `git diff HEAD` empty: - `HomeAppsStrip.tsx`: `appTileColors(...)` replaced by `undefined` → branding test `2 failed | 1 passed (3)` (the gradient control stays green). - `GanttView.tsx`: `taskColumnLabel ||` removed → heading test `1 failed | 3 passed (4)` (the three generic controls stay green). </details> <details><summary>Local gates on 98f7880: dispatch-gates derived 35, all run, all exit 0</summary> `node scripts/pm/dispatch-gates.mjs --ran` → `35 derived famil(ies) accounted for — 35 run, 0 NOT-MEASURED`. Outside the derivation: `pnpm check:changeset-fixed` (its roster lives under `.changeset/`), and `type-check` for both touched packages. `pnpm --dir ui check:new-line-citations` → `0 new citation(s)`. ```text node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 node scripts/check-empty-changeset.mjs --self-test :: exit 0 node scripts/check-sdui-manifest.mjs :: exit 0 node scripts/check-sdui-manifest.mjs --self-test :: exit 0 node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 node scripts/release-pending-publish.mjs --self-test :: exit 0 pnpm --dir ui exec vitest run scripts/ :: exit 0 pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 pnpm check:changeset-gate-self-tests :: exit 0 pnpm check:closed-ceremony-text :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:dispatcher-error-vocabulary :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:engine-double-contract :: exit 0 pnpm check:error-status-conformance :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:pm-changeset-deadline-census :: exit 0 pnpm check:published-files :: exit 0 pnpm check:query-options-erasure :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:skill-identifier-liveness :: exit 0 pnpm check:test-source-alias :: exit 0 pnpm check:tier-file-adoption :: exit 0 pnpm check:type-check-coverage :: exit 0 pnpm check:type-check-debt :: exit 0 pnpm check:undeclared-dep-imports :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm check:where-matcher :: exit 0 pnpm check:changeset-fixed :: exit 0 pnpm --filter @object-ui/app-shell run type-check :: exit 0 pnpm --filter @object-ui/plugin-gantt run type-check :: exit 0 ``` Tests: the two new files `7 passed (7)`; with neighbours (every `console/home/__tests__/` file, `AppSwitcher.appIdentity-12201`, `ObjectGantt.exportFileName.i18n`, `GanttView.layout`) `Test Files 37 passed (37)`, `Tests 242 passed (242)`. CI families: this PR's check-runs. </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Utp6CYw9w9VeYi5rKNEsNK --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3f02da1 commit 7a37823

9 files changed

Lines changed: 295 additions & 39 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
'@object-ui/plugin-gantt': patch
4+
---
5+
6+
fix(app-shell, plugin-gantt): the home launcher tile draws the app's branding, and a gantt's name column is headed by its object
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
The console home's app tiles (and the `app:launcher` block, which draws the same strip) now follow the app's declared branding, the way the top-bar app pill and the app switcher already do: the `branding.logo` image when one is declared, else the app's icon on its `branding.primaryColor` when that is a hex, else the icon on the name-keyed gradient as before. A logo that fails to load falls back to the tile.
12+
13+
A gantt bound to an object now heads its task list's name column with that object's label (translated like every other surface that names the object), so a campaign gantt reads "Campaign" rather than "Task Name". An inline data set, an object that declares no label, and a mixed-object tree (`objectField`) keep the generic heading.

‎ui/packages/app-shell/src/console/home/HomeAppsStrip.tsx‎

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@
2222
*
2323
* The strip's look is the Home artboard's apps section (objectui#12601): a
2424
* 15 px heading with a count pill, 112 px tiles with a 56 px icon on the app's
25-
* colour and a 13 px name on one line. Three optional parts ride along, all
25+
* colour and a 13 px name on one line. The icon square follows the app's own
26+
* branding, the way `AppMark` does in the top bar and the switcher: its
27+
* `branding.logo` when it declares one, else its `icon` on its
28+
* `branding.primaryColor` when that is a hex, else its `icon` on the
29+
* name-keyed gradient. A logo that fails to load falls back to the tile. Three optional parts ride along, all
2630
* off unless the host asks: an 11 px "2 hr. ago" line under a tile's
2731
* name (`lastOpened`, which Home reads from the recent-items store), a muted
2832
* "System Settings" link at the heading's right (`settingsHref` — Home moves
@@ -32,7 +36,7 @@
3236
*
3337
* @module
3438
*/
35-
import { useMemo, useState } from 'react';
39+
import { useMemo, useState, type ReactNode } from 'react';
3640
import { Link } from 'react-router-dom';
3741
import { LayoutGrid, Plus, Settings, Star } from 'lucide-react';
3842
import { cn } from '@object-ui/components';
@@ -43,13 +47,58 @@ import { timeAgo } from '../../utils/relativeTime.js';
4347
import { TILE_OPENED_TIME_FORMAT } from './appLastOpened.js';
4448
import type { FavoriteItem } from '../../hooks/useFavorites.js';
4549
import { ICON_BOX_CLASSES, TILE_CLASSES, launcherIconGradient } from './launcherTile.js';
50+
import { BRANDED_TILE_CLASSES, appTileColors, appTileStyle } from '../../layout/appTileColors.js';
4651

4752
const COMPACT_LIMIT = 19;
4853

4954
/** The 12 px links at the heading's right. */
5055
const HEADER_LINK_CLASSES =
5156
'inline-flex items-center gap-1.5 rounded-sm text-xs transition-colors hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring';
5257

58+
/**
59+
* One app's 56 px icon square, drawn from the app's branding (see the module
60+
* header). `children` is the favourite badge, which rides on every face.
61+
*/
62+
function LauncherAppIcon({ app, children }: { app: any; children?: ReactNode }) {
63+
const logo = typeof app?.branding?.logo === 'string' && app.branding.logo ? app.branding.logo : undefined;
64+
// The URL that failed to load, so a later, different logo is tried afresh.
65+
const [failedLogo, setFailedLogo] = useState<string | undefined>(undefined);
66+
const box = cn(ICON_BOX_CLASSES, 'shadow-sm transition-transform group-hover:scale-105');
67+
68+
if (logo && logo !== failedLogo) {
69+
return (
70+
<span className={cn(box, 'bg-card')} data-testid={`app-tile-logo-${app.name}`}>
71+
<img
72+
src={logo}
73+
alt=""
74+
aria-hidden
75+
onError={() => setFailedLogo(logo)}
76+
className="size-full rounded-[14px] object-contain"
77+
/>
78+
{children}
79+
</span>
80+
);
81+
}
82+
83+
const Icon = getIcon(app?.icon);
84+
const colors = appTileColors(app?.branding?.primaryColor);
85+
return (
86+
<span
87+
className={cn(
88+
box,
89+
colors ? BRANDED_TILE_CLASSES : cn('bg-gradient-to-br text-white', launcherIconGradient(app.name)),
90+
)}
91+
style={colors ? appTileStyle(colors) : undefined}
92+
data-testid={`app-tile-icon-${app.name}`}
93+
data-branded={colors ? 'true' : undefined}
94+
>
95+
{/* eslint-disable-next-line react-hooks/static-components -- getIcon returns a stable icon component from a static registry, not one created during render */}
96+
<Icon className="size-[26px]" />
97+
{children}
98+
</span>
99+
);
100+
}
101+
53102
export function HomeAppsStrip({
54103
apps,
55104
favorites,
@@ -169,10 +218,8 @@ export function HomeAppsStrip({
169218
{/* `-ml-2` puts the first icon under the heading: a tile pads its icon 8 px. */}
170219
<div className="-ml-2 flex flex-wrap gap-1">
171220
{visible.map((app) => {
172-
const Icon = getIcon(app.icon);
173221
const label = appLabel({ name: app.name, label: resolveKeyedI18nLabel(app.label, t) });
174222
const fav = favNames.has(app.name);
175-
const grad = launcherIconGradient(app.name);
176223
const openedAgo = timeAgo(lastOpened?.get(app.name), displayLocale, TILE_OPENED_TIME_FORMAT);
177224
return (
178225
<Link
@@ -183,20 +230,13 @@ export function HomeAppsStrip({
183230
className={TILE_CLASSES}
184231
data-testid={`app-tile-${app.name}`}
185232
>
186-
<span
187-
className={cn(
188-
ICON_BOX_CLASSES,
189-
'bg-gradient-to-br text-white shadow-sm transition-transform group-hover:scale-105',
190-
grad,
191-
)}
192-
>
193-
<Icon className="size-[26px]" />
233+
<LauncherAppIcon app={app}>
194234
{fav && (
195235
<span className="absolute -right-1 -top-1 inline-flex h-4 w-4 items-center justify-center rounded-full bg-amber-400 ring-2 ring-background">
196236
<Star className="h-2.5 w-2.5 fill-white text-white" />
197237
</span>
198238
)}
199-
</span>
239+
</LauncherAppIcon>
200240
<span className="w-full truncate text-[13px] font-medium">{label}</span>
201241
{openedAgo && (
202242
<span
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* objectstack#27386 row 4 — a launcher tile draws the app's own branding.
5+
*
6+
* The 56 px icon square on Home (and the `app:launcher` block, which mounts the
7+
* same strip) follows the ladder `AppMark` uses in the top bar and the app
8+
* switcher: `branding.logo` when declared, else the app's `icon` on its
9+
* `branding.primaryColor` when that is a hex, else the `icon` on the name-keyed
10+
* gradient. A logo that fails to load falls back to the tile.
11+
*/
12+
13+
import '@testing-library/jest-dom/vitest';
14+
import { describe, it, expect, afterEach } from 'vitest';
15+
import React from 'react';
16+
import { render, screen, fireEvent, cleanup } from '@testing-library/react';
17+
import { MemoryRouter } from 'react-router-dom';
18+
import { HomeAppsStrip } from '../HomeAppsStrip';
19+
import { launcherIconGradient } from '../launcherTile';
20+
import { appTileColors } from '../../../layout/appTileColors';
21+
22+
function renderStrip(apps: Record<string, unknown>[], favorites: { id: string }[] = []) {
23+
return render(
24+
<MemoryRouter initialEntries={['/home']}>
25+
<HomeAppsStrip
26+
apps={apps}
27+
favorites={favorites as any}
28+
onBrowseMarketplace={() => {}}
29+
isAdmin={false}
30+
/>
31+
</MemoryRouter>,
32+
);
33+
}
34+
35+
describe('HomeAppsStrip — tile branding (objectstack#27386 row 4)', () => {
36+
afterEach(cleanup);
37+
38+
it('draws a declared hex primaryColor instead of the name-keyed gradient', () => {
39+
renderStrip([{ name: 'hotcrm', label: 'HotCRM', icon: 'Flame', branding: { primaryColor: '#ea580c' } }]);
40+
const icon = screen.getByTestId('app-tile-icon-hotcrm');
41+
expect(icon).toHaveAttribute('data-branded', 'true');
42+
expect(icon.className).not.toContain('bg-gradient-to-br');
43+
for (const cls of launcherIconGradient('hotcrm').split(' ')) expect(icon.className).not.toContain(cls);
44+
const colors = appTileColors('#ea580c')!;
45+
expect(icon.style.getPropertyValue('--os-app-tile-bg')).toBe(colors.bg);
46+
expect(icon.style.getPropertyValue('--os-app-tile-fg')).toBe(colors.fg);
47+
expect(icon.style.getPropertyValue('--os-app-tile-bg-dark')).toBe(colors.bgDark);
48+
expect(icon.style.getPropertyValue('--os-app-tile-fg-dark')).toBe(colors.fgDark);
49+
});
50+
51+
it('keeps the name-keyed gradient when no usable colour is declared', () => {
52+
renderStrip([
53+
{ name: 'plain', label: 'Plain', icon: 'Box' },
54+
// Not a hex: the spec's colour is a hex code, and `AppMark` reads only that.
55+
{ name: 'named', label: 'Named', icon: 'Box', branding: { primaryColor: 'orange' } },
56+
]);
57+
for (const name of ['plain', 'named']) {
58+
const icon = screen.getByTestId(`app-tile-icon-${name}`);
59+
expect(icon).not.toHaveAttribute('data-branded');
60+
expect(icon.className).toContain('bg-gradient-to-br');
61+
for (const cls of launcherIconGradient(name).split(' ')) expect(icon.className).toContain(cls);
62+
expect(icon.getAttribute('style')).toBeNull();
63+
}
64+
});
65+
66+
it('draws a declared logo, and falls back to the coloured tile when it fails to load', () => {
67+
renderStrip(
68+
[{ name: 'hotcrm', label: 'HotCRM', icon: 'Flame', branding: { logo: '/logo.png', primaryColor: '#ea580c' } }],
69+
[{ id: 'app:hotcrm' }],
70+
);
71+
const logoBox = screen.getByTestId('app-tile-logo-hotcrm');
72+
const img = logoBox.querySelector('img');
73+
expect(img).toHaveAttribute('src', '/logo.png');
74+
expect(screen.queryByTestId('app-tile-icon-hotcrm')).toBeNull();
75+
// The favourite badge rides on the logo face too.
76+
expect(logoBox.querySelector('.bg-amber-400')).not.toBeNull();
77+
78+
fireEvent.error(img!);
79+
expect(screen.queryByTestId('app-tile-logo-hotcrm')).toBeNull();
80+
const icon = screen.getByTestId('app-tile-icon-hotcrm');
81+
expect(icon).toHaveAttribute('data-branded', 'true');
82+
expect(icon.querySelector('.bg-amber-400')).not.toBeNull();
83+
});
84+
});

‎ui/packages/app-shell/src/console/home/launcherTile.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ function hashStr(s: string): number {
3333
return Math.abs(h);
3434
}
3535

36-
/** The icon gradient for a tile, chosen by a stable key (an app's `name`). */
36+
/**
37+
* The icon gradient for a tile, chosen by a stable key (an app's `name`). An
38+
* app tile draws it only when the app declares no branding (`HomeAppsStrip`).
39+
*/
3740
export function launcherIconGradient(key: string): string {
3841
return ICON_GRADIENTS[hashStr(key) % ICON_GRADIENTS.length];
3942
}

‎ui/packages/app-shell/src/layout/AppMark.tsx‎

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import * as React from 'react';
2424
import { cn } from '@object-ui/components';
2525
import { getIcon } from '../utils/getIcon.js';
26-
import { appTileColors } from './appTileColors.js';
26+
import { BRANDED_TILE_CLASSES, appTileColors, appTileStyle } from './appTileColors.js';
2727

2828
/** The app fields this glyph reads — the spec's `AppSchema` names. */
2929
export interface AppMarkApp {
@@ -42,16 +42,6 @@ const SIZE_CLASSES: Record<AppMarkSize, { box: string; glyph: string }> = {
4242
md: { box: 'size-[26px] rounded-md', glyph: 'size-4' },
4343
};
4444

45-
/**
46-
* The tile for a declared `primaryColor`. The utility strings are literals so
47-
* Tailwind emits them at build time; the author's colour reaches them only as
48-
* CSS custom properties, so `dark:` stays a real variant the theme controls
49-
* (AGENTS.md section 2, the author-declared colour carve-out).
50-
*/
51-
const BRANDED_TILE_CLASSES =
52-
'bg-[color:var(--os-app-tile-bg)] text-[color:var(--os-app-tile-fg)] ' +
53-
'dark:bg-[color:var(--os-app-tile-bg-dark)] dark:text-[color:var(--os-app-tile-fg-dark)]';
54-
5545
/** The tile for an app that declares no usable colour — theme tokens only. */
5646
const NEUTRAL_TILE_CLASSES = 'border bg-muted text-muted-foreground';
5747

@@ -92,16 +82,7 @@ export function AppMark({ app, size = 'sm', className }: AppMarkProps) {
9282
colors ? BRANDED_TILE_CLASSES : NEUTRAL_TILE_CLASSES,
9383
className,
9484
)}
95-
style={
96-
colors
97-
? ({
98-
'--os-app-tile-bg': colors.bg,
99-
'--os-app-tile-fg': colors.fg,
100-
'--os-app-tile-bg-dark': colors.bgDark,
101-
'--os-app-tile-fg-dark': colors.fgDark,
102-
} as React.CSSProperties)
103-
: undefined
104-
}
85+
style={colors ? appTileStyle(colors) : undefined}
10586
>
10687
{/* eslint-disable-next-line react-hooks/static-components -- getIcon returns a stable icon component from a static registry, not one created during render */}
10788
<Icon className={sizeClasses.glyph} />

‎ui/packages/app-shell/src/layout/appTileColors.ts‎

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
/**
22
* The fallback app tile's colours, derived from an app's declared
3-
* `branding.primaryColor` (objectui#12201). `AppMark` publishes them as CSS
4-
* custom properties on the tile; see that component for why.
3+
* `branding.primaryColor` (objectui#12201). `AppMark` and the home launcher's
4+
* tiles publish them as CSS custom properties on the tile (`appTileStyle`),
5+
* which the static `BRANDED_TILE_CLASSES` utilities consume.
56
*
67
* @module
78
*/
89

10+
import type { CSSProperties } from 'react';
911
import { contrastRatio, hexToHSL } from '@object-ui/core';
1012

1113
/** The two foregrounds `AppShell` pairs with a brand colour (`--primary-foreground`). */
@@ -47,3 +49,23 @@ export function appTileColors(primaryColor: unknown): AppTileColors | undefined
4749
fgDark: ON_BRAND_DARK,
4850
};
4951
}
52+
53+
/**
54+
* The tile for a declared `primaryColor`. The utility strings are literals so
55+
* Tailwind emits them at build time; the author's colour reaches them only as
56+
* CSS custom properties, so `dark:` stays a real variant the theme controls
57+
* (AGENTS.md section 2, the author-declared colour carve-out).
58+
*/
59+
export const BRANDED_TILE_CLASSES =
60+
'bg-[color:var(--os-app-tile-bg)] text-[color:var(--os-app-tile-fg)] ' +
61+
'dark:bg-[color:var(--os-app-tile-bg-dark)] dark:text-[color:var(--os-app-tile-fg-dark)]';
62+
63+
/** The custom properties `BRANDED_TILE_CLASSES` reads, for a tile's `style`. */
64+
export function appTileStyle(colors: AppTileColors): CSSProperties {
65+
return {
66+
'--os-app-tile-bg': colors.bg,
67+
'--os-app-tile-fg': colors.fg,
68+
'--os-app-tile-bg-dark': colors.bgDark,
69+
'--os-app-tile-fg-dark': colors.fgDark,
70+
} as CSSProperties;
71+
}

‎ui/packages/plugin-gantt/src/GanttView.tsx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,11 @@ export interface GanttViewProps {
735735
* overwriting each other in the Downloads folder.
736736
*/
737737
exportFileName?: string
738+
/**
739+
* Heading of the task list's name column — what the rows are, e.g. the bound
740+
* object's label ("Campaign"). Falls back to the generic "Task Name".
741+
*/
742+
taskColumnLabel?: string
738743
/**
739744
* Per-interaction switches. Omit for all-on. See
740745
* {@link GanttInteractions}: e.g. `{ resize: false }` keeps bars movable and
@@ -928,6 +933,7 @@ export function GanttView({
928933
dependencyTypes = true,
929934
timeZone,
930935
exportFileName,
936+
taskColumnLabel,
931937
interactions,
932938
onBeforeTaskUpdate,
933939
}: GanttViewProps) {
@@ -3821,7 +3827,7 @@ export function GanttView({
38213827
className="flex items-center font-medium text-xs text-muted-foreground px-2 sm:px-4 border-r bg-card z-20 shadow-sm"
38223828
style={{ width: taskListWidth, minWidth: taskListWidth }}
38233829
>
3824-
<div className="flex-1 truncate">{t('gantt.column.taskName')}</div>
3830+
<div className="flex-1 truncate" data-testid="gantt-task-column-heading">{taskColumnLabel || t('gantt.column.taskName')}</div>
38253831
{showSEColumns && (
38263832
<>
38273833
<div className="w-16 gantt-sm-w20 text-right">{t('gantt.column.start')}</div>

0 commit comments

Comments
 (0)