Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 34 additions & 20 deletions packages/boxel-ui/addon/src/components/tabbed-header/index.gts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface Signature {
Args: {
activeTabId?: string;
headerBackgroundColor?: string;
headerTitle: string;
headerTitle?: string;
setActiveTab: (tabId: string) => void;
tabs: Array<{
displayName: string;
Expand All @@ -31,17 +31,19 @@ export default class TabbedHeader extends Component<Signature> {
<header
class='app-header'
style={{cssVar
header-background-color=@headerBackgroundColor
header-text-color=(getContrastColor @headerBackgroundColor)
boxel-header-background=@headerBackgroundColor
boxel-header-foreground=(getContrastColor @headerBackgroundColor)
}}
...attributes
>
<div class='app-title-group'>
{{#if (has-block 'headerIcon')}}
{{yield to='headerIcon'}}
{{/if}}
<h1 class='app-title'>{{@headerTitle}}</h1>
</div>
{{#if @headerTitle}}
<div class='app-title-group'>
{{#if (has-block 'headerIcon')}}
{{yield to='headerIcon'}}
{{/if}}
<h1 class='app-title'>{{@headerTitle}}</h1>
</div>
{{/if}}

<div class='app-content'>
<nav class='app-nav'>
Expand Down Expand Up @@ -70,9 +72,18 @@ export default class TabbedHeader extends Component<Signature> {
</header>
<style scoped>
.app-header {
padding: 0 var(--boxel-sp-lg);
background-color: var(--header-background-color, var(--boxel-light));
color: var(--header-text-color, var(--boxel-dark));
--_header-background-color: var(
--boxel-header-background,
var(--sidebar, var(--card))
);
--_header-text-color: var(
--boxel-header-foreground,
var(--sidebar-foreground, var(--card-foreground))
);
padding-top: var(--boxel-sp-3xs);
padding-inline: var(--boxel-sp-lg);
background-color: var(--_header-background-color);
color: var(--_header-text-color);
}
.app-title-group {
padding: var(--boxel-sp-xs) 0;
Expand All @@ -82,9 +93,10 @@ export default class TabbedHeader extends Component<Signature> {
}
.app-title {
margin: 0;
font: 900 var(--boxel-font);
letter-spacing: var(--boxel-lsp-xl);
text-transform: uppercase;
font-weight: var(--boxel-header-title-font-weight, 900);
font-size: var(--boxel-header-title-font-size, var(--boxel-font-size));
letter-spacing: var(--boxel-header-title-lsp, var(--boxel-lsp-xl));
text-transform: var(--boxel-header-title-transform, uppercase);
}
.app-content {
display: flex;
Expand All @@ -94,7 +106,8 @@ export default class TabbedHeader extends Component<Signature> {
gap: var(--boxel-sp-lg);
}
.app-nav {
font: 500 var(--boxel-font-sm);
font-size: var(--boxel-font-size-sm);
font-weight: 500;
letter-spacing: var(--boxel-lsp-sm);
flex: 1;
}
Expand All @@ -107,20 +120,21 @@ export default class TabbedHeader extends Component<Signature> {
flex-flow: row wrap;
}
.app-tab-list a {
display: block;
height: 100%;
padding: var(--boxel-sp-xs) var(--boxel-sp-xxs);
padding: var(--boxel-sp-xs) var(--boxel-sp-2xs);
border-bottom: 4px solid transparent;
transition:
border-bottom-color 0.3s ease-in-out,
font-weight 0.3s ease-in-out;
}
Comment on lines 126 to 130
.app-tab-list a.active {
color: var(--header-text-color, var(--boxel-dark));
border-bottom-color: var(--header-text-color, var(--boxel-dark));
color: var(--_header-text-color);
border-bottom-color: var(--_header-text-color);
font-weight: 600;
}
.app-tab-list a:hover:not(:disabled) {
color: var(--header-text-color, var(--boxel-dark));
color: var(--_header-text-color);
font-weight: 600;
}
/* this prevents layout shift when text turns bold on hover/active */
Expand Down
186 changes: 129 additions & 57 deletions packages/boxel-ui/addon/src/components/tabbed-header/usage.gts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import type Owner from '@ember/owner';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import FreestyleUsage from 'ember-freestyle/components/freestyle/usage';
import {
type CSSVariableInfo,
cssVariable,
} from 'ember-freestyle/decorators/css-variable';

import cssVar from '../../helpers/css-var.ts';
import Icon from '../../icons/sparkle.gts';
import BoxelInput from '../input/index.gts';
import TabbedHeader from './index.gts';
Expand Down Expand Up @@ -36,6 +41,15 @@ export default class TabbedHeaderUsage extends Component {
@tracked headerColor = '#ffd800';
@tracked searchValue = '';

@cssVariable({ cssClassName: 'tabbed-header-freestyle-container' })
declare headerTitleFontWeight: CSSVariableInfo;
@cssVariable({ cssClassName: 'tabbed-header-freestyle-container' })
declare headerTitleFontSize: CSSVariableInfo;
@cssVariable({ cssClassName: 'tabbed-header-freestyle-container' })
declare headerTitleLsp: CSSVariableInfo;
@cssVariable({ cssClassName: 'tabbed-header-freestyle-container' })
declare headerTitleTransform: CSSVariableInfo;

constructor(owner: Owner, args: any) {
super(owner, args);
this.activeTabId = window.location?.hash?.slice(1) ?? this.tabs[0]?.tabId;
Expand All @@ -44,63 +58,121 @@ export default class TabbedHeaderUsage extends Component {
setActiveTab = (tabId: string) => (this.activeTabId = tabId);

<template>
<FreestyleUsage
@name='TabbedHeader'
@description='Header row with horizontally arranged tab buttons that switch between sections — pair with content panels below for the standard tabs pattern.'
<div
class='tabbed-header-freestyle-container'
style={{cssVar
boxel-header-title-font-weight=this.headerTitleFontWeight.value
boxel-header-title-font-size=this.headerTitleFontSize.value
boxel-header-title-lsp=this.headerTitleLsp.value
boxel-header-title-transform=this.headerTitleTransform.value
}}
>
<:example>
<TabbedHeader
@headerTitle={{this.headerTitle}}
@tabs={{this.tabs}}
@setActiveTab={{this.setActiveTab}}
@activeTabId={{this.activeTabId}}
@headerBackgroundColor={{this.headerColor}}
>
<:headerIcon>
<Icon width='25' height='25' role='presentation' />
</:headerIcon>
<:sideContent>
<BoxelInput
@type='search'
@value={{this.searchValue}}
@onInput={{fn (mut this.searchValue)}}
placeholder='Search...'
/>
</:sideContent>
</TabbedHeader>
</:example>
<:api as |Args|>
<Args.String
@name='headerTitle'
@description='Title to be displayed on the header'
@value={{this.headerTitle}}
@onInput={{fn (mut this.headerTitle)}}
@required={{true}}
/>
<Args.Object
@name='tabs'
@description='Tabs for navigation'
@value={{this.tabs}}
@onInput={{fn (mut this.tabs)}}
/>
<Args.String
@name='activeTabId'
@description='Id of the active tab'
@value={{this.activeTabId}}
@onInput={{fn (mut this.activeTabId)}}
/>
<Args.Action
@name='setActiveTab'
@description='Action to be called when a tab is clicked'
/>
<Args.String
@name='headerBackgroundColor'
@description='3-or-6 digit hex color code for background color'
@value={{this.headerColor}}
@onInput={{fn (mut this.headerColor)}}
@defaultValue='#ffffff'
/>
</:api>
</FreestyleUsage>
<FreestyleUsage
@name='TabbedHeader'
@description='Header row with horizontally arranged tab buttons that switch between sections — pair with content panels below for the standard tabs pattern. The title row is optional; omit headerTitle to render just the tab bar.'
>
<:example>
<TabbedHeader
@headerTitle={{this.headerTitle}}
@tabs={{this.tabs}}
@setActiveTab={{this.setActiveTab}}
@activeTabId={{this.activeTabId}}
@headerBackgroundColor={{this.headerColor}}
>
<:headerIcon>
<Icon width='25' height='25' role='presentation' />
</:headerIcon>
<:sideContent>
<BoxelInput
@type='search'
@value={{this.searchValue}}
@onInput={{fn (mut this.searchValue)}}
placeholder='Search...'
/>
</:sideContent>
</TabbedHeader>
</:example>
<:api as |Args|>
<Args.String
@name='headerTitle'
@description='Title to be displayed on the header. When omitted, the title row (including the headerIcon block) is not rendered.'
@value={{this.headerTitle}}
@onInput={{fn (mut this.headerTitle)}}
/>
<Args.Object
@name='tabs'
@description='Tabs for navigation. Each entry is { displayName, tabId }.'
@value={{this.tabs}}
@onInput={{fn (mut this.tabs)}}
/>
<Args.String
@name='activeTabId'
@description='Id of the active tab'
@value={{this.activeTabId}}
@onInput={{fn (mut this.activeTabId)}}
/>
<Args.Action
@name='setActiveTab'
@description='Action to be called when a tab is clicked; receives the clicked tabId.'
/>
<Args.String
@name='headerBackgroundColor'
@description='3-or-6 digit hex color code for the background color. The foreground/text color is derived automatically for contrast.'
@value={{this.headerColor}}
@onInput={{fn (mut this.headerColor)}}
/>
<Args.Yield
@name='headerIcon'
@description='Block rendered before the title; typically an icon. Only shown when headerTitle is present.'
/>
<Args.Yield
@name='sideContent'
@description='Block rendered at the end of the tab row; typically a search input or actions.'
/>
</:api>
<:cssVars as |Css|>
<Css.Basic
@name='boxel-header-background'
@description='Header background color. Set automatically from @headerBackgroundColor; falls back to var(--sidebar, var(--card)).'
/>
<Css.Basic
@name='boxel-header-foreground'
@description='Header text color. Derived for contrast from @headerBackgroundColor; falls back to var(--sidebar-foreground, var(--card-foreground)).'
/>
<Css.Basic
@name='boxel-header-title-font-weight'
@type='font-weight'
@description='Font weight of the header title (default 900)'
@defaultValue={{this.headerTitleFontWeight.defaults}}
@value={{this.headerTitleFontWeight.value}}
@onInput={{this.headerTitleFontWeight.update}}
/>
<Css.Basic
@name='boxel-header-title-font-size'
@type='length'
@description='Font size of the header title'
@defaultValue={{this.headerTitleFontSize.defaults}}
@value={{this.headerTitleFontSize.value}}
@onInput={{this.headerTitleFontSize.update}}
/>
<Css.Basic
@name='boxel-header-title-lsp'
@type='length'
@description='Letter spacing of the header title'
@defaultValue={{this.headerTitleLsp.defaults}}
@value={{this.headerTitleLsp.value}}
@onInput={{this.headerTitleLsp.update}}
/>
<Css.Basic
@name='boxel-header-title-transform'
@type='text-transform'
@description='Text transform of the header title (e.g. uppercase, none)'
@defaultValue={{this.headerTitleTransform.defaults}}
@value={{this.headerTitleTransform.value}}
@onInput={{this.headerTitleTransform.update}}
/>
</:cssVars>
</FreestyleUsage>
</div>
</template>
}
55 changes: 32 additions & 23 deletions packages/software-factory/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,18 @@ The orchestrator (`runIssueLoop`) is a thin scheduler that picks the next unbloc

### Target Realm Artifact Structure

| Path | What it is |
| --------------------- | -------------------------------------------------------------------- |
| `Projects/` | Project card with objective, scope, success criteria |
| `Issues/` | Issue cards — bootstrap seed + implementation issues |
| `Knowledge Articles/` | Context articles derived from the brief |
| `*.gts` | Card definition files |
| `*.test.gts` | Co-located QUnit test files |
| `CardName/` | Sample card instances with realistic data |
| `Spec/` | Catalog Spec cards linking to card definitions and sample instances |
| `Validations/` | Validation artifacts — TestRun cards (test results) and lint results |
| Path | What it is |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `index.json` | `RealmDashboard` dashboard the realm opens to — Overview / Board / Artifacts tabs (only for realms the factory created) |
| `cards-grid.json` | `CardsGrid` instance the Artifacts tab renders |
| `Projects/` | Project card with objective, scope, success criteria |
| `Issues/` | Issue cards — bootstrap seed + implementation issues |
| `Knowledge Articles/` | Context articles derived from the brief |
| `*.gts` | Card definition files |
| `*.test.gts` | Co-located QUnit test files |
| `CardName/` | Sample card instances with realistic data |
| `Spec/` | Catalog Spec cards linking to card definitions and sample instances |
| `Validations/` | Validation artifacts — TestRun cards (test results) and lint results |

## Prerequisites

Expand Down Expand Up @@ -115,18 +117,24 @@ pnpm factory:go \

### What to expect in the Boxel host app (target realm)

| Folder / File | What it is |
| ---------------------------- | ------------------------------------------------------------------------ |
| `Projects/` | A Project card with the brief's objective and success criteria |
| `Issues/bootstrap-seed` | Bootstrap issue — status `done`, issueType `bootstrap` |
| `Issues/<slug>-define-card` | Implementation issue #1 — card definition + tests |
| `Issues/<slug>-catalog-spec` | Implementation issue #2 — catalog spec + examples |
| `Knowledge Articles/` | Brief context and agent onboarding articles |
| `*.gts` | Card definition file(s) for the implemented card |
| `*.test.gts` | Co-located QUnit test file(s) |
| `CardName/` | Sample card instance(s) with realistic data |
| `Spec/` | Catalog Spec card(s) linking to the card definition and sample instances |
| `Validations/` | Validation artifacts — TestRun cards and lint results (pass/fail) |
A realm the factory created opens to the **Overview dashboard** (`index.json`, a
`RealmDashboard` card) rather than a bare card grid. It has three tabs:
Overview (project status, setup roadmap, issue KPIs, validation runs), Board
(the kanban IssueTracker), and Artifacts (the CardsGrid of everything created).

| Folder / File | What it is |
| ---------------------------- | ----------------------------------------------------------------------------- |
| `index.json` | The Overview dashboard the realm opens to (Overview / Board / Artifacts tabs) |
| `Projects/` | A Project card with the brief's objective and success criteria |
| `Issues/bootstrap-seed` | Bootstrap issue — status `done`, issueType `bootstrap` |
| `Issues/<slug>-define-card` | Implementation issue #1 — card definition + tests |
| `Issues/<slug>-catalog-spec` | Implementation issue #2 — catalog spec + examples |
| `Knowledge Articles/` | Brief context and agent onboarding articles |
| `*.gts` | Card definition file(s) for the implemented card |
| `*.test.gts` | Co-located QUnit test file(s) |
| `CardName/` | Sample card instance(s) with realistic data |
| `Spec/` | Catalog Spec card(s) linking to the card definition and sample instances |
| `Validations/` | Validation artifacts — TestRun cards and lint results (pass/fail) |

## Architecture

Expand All @@ -141,7 +149,8 @@ factory:go → createSeedIssue() → runIssueLoop()
Key modules:

- `src/factory-entrypoint.ts` — CLI entrypoint, creates seed issue + runs issue loop
- `src/factory-seed.ts` — creates the bootstrap seed issue in the realm
- `src/factory-seed.ts` — creates the bootstrap seed issue in the realm; links its `project` once the bootstrap issue creates one
- `src/factory-realm-index.ts` — writes the `RealmDashboard` dashboard for a freshly-created realm and links its `board` to the bootstrap IssueTracker
- `src/factory-issue-loop-wiring.ts` — constructs all loop infrastructure (auth, tools, agent, validator)
- `src/issue-loop.ts` — the two-level issue-driven loop (outer: issues, inner: iterations with validation)
- `src/issue-scheduler.ts` — issue selection with priority/dependency ordering
Expand Down
Loading
Loading