diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1fca9e45..eefbee21 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,78 +10,82 @@ on: workflow_dispatch: jobs: - build: + dotnet: + name: .NET samples runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v2 + - name: Checkout code + uses: actions/checkout@v4 - - name: Setup .NET - uses: actions/setup-dotnet@v2 - with: - dotnet-version: '10.0.x' # Use .NET 10 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json - - name: Restore dependencies - run: dotnet restore Samples.slnx + - name: Restore dependencies + run: dotnet restore Samples.slnx - - name: Build - run: dotnet build Samples.slnx --configuration Release --no-restore + - name: Build Debug and generate proxies + run: dotnet build Samples.slnx --configuration Debug --no-restore - - name: Run tests - run: dotnet test Samples.slnx --configuration Release --no-build --verbosity normal + - name: Run focused Debug specifications + run: | + for project in \ + Arc/React/Arc.React.Specs/Arc.React.Specs.csproj \ + Chronicle/Backend/Backend.Specs/Backend.Specs.csproj \ + Chronicle/CrossStore/CrossStore.Specs/CrossStore.Specs.csproj \ + Chronicle/MultiTenancy/MultiTenancy.Specs/MultiTenancy.Specs.csproj \ + Chronicle/OperationsDiagnosis/OperationsDiagnosis.Specs/OperationsDiagnosis.Specs.csproj \ + Chronicle/Processing/Processing.Specs/Processing.Specs.csproj + do + dotnet test "$project" --configuration Debug --no-build --verbosity normal + done - frontend-install: - runs-on: ubuntu-latest + - name: Build Release without regenerating proxies + run: dotnet build Samples.slnx --configuration Release --no-restore -p:CratisProxiesOutputPath= - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: '20' - - uses: actions/cache@v3 - id: yarn-cache - with: - path: | - .yarn/cache - **/node_modules - **/.eslintcache - **/yarn.lock - key: ${{ runner.os }}-yarn-${{ hashFiles('**/package.json') }} - - name: Install dependencies - run: yarn - - frontend-build: + frontend: + name: Frontend — ${{ matrix.project }} runs-on: ubuntu-latest - needs: frontend-install strategy: + fail-fast: false matrix: - project: [Library] + project: + - Arc/React + - Library/Lending + - Library/Members steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: '20' - - uses: actions/cache@v3 - id: yarn-cache - with: - path: | - .yarn/cache - **/node_modules - **/.eslintcache - **/yarn.lock - key: ${{ runner.os }}-yarn-${{ hashFiles('**/package.json') }} - - - name: Install dependencies - run: yarn - - - name: Build ${{ matrix.project }} - run: yarn build - working-directory: ${{ matrix.project }} + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '23' + + - name: Enable Corepack + run: corepack enable + + - name: Install dependencies + run: yarn install + + - name: Validate sample catalog + run: yarn samples:validate + + - name: Lint ${{ matrix.project }} + working-directory: ${{ matrix.project }} + run: yarn lint:ci + + - name: Compile ${{ matrix.project }} + working-directory: ${{ matrix.project }} + run: yarn compile + + - name: Test ${{ matrix.project }} + working-directory: ${{ matrix.project }} + run: yarn test --passWithNoTests + + - name: Build ${{ matrix.project }} + working-directory: ${{ matrix.project }} + run: yarn build diff --git a/.github/workflows/update-dependencies.yml b/.github/workflows/update-dependencies.yml index 3bca81ad..0568e9fc 100644 --- a/.github/workflows/update-dependencies.yml +++ b/.github/workflows/update-dependencies.yml @@ -70,7 +70,10 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - folder: [Library] + folder: + - Arc/React + - Library/Lending + - Library/Members steps: - name: Checkout repository uses: actions/checkout@v4 @@ -80,7 +83,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '23' - name: Pull latest changes run: git pull --rebase diff --git a/Arc/React/.frontend/App.tsx b/Arc/React/.frontend/App.tsx new file mode 100644 index 00000000..89f364e2 --- /dev/null +++ b/Arc/React/.frontend/App.tsx @@ -0,0 +1,18 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { Arc } from '@cratis/arc.react'; +import { DialogComponents } from '@cratis/arc.react/dialogs'; +import { CratisComponentsProvider } from '@cratis/components/Common'; +import { BusyIndicatorDialog, ConfirmationDialog } from '@cratis/components/Dialogs'; +import { Board } from '../Ideas/Board/Board'; + +export const App = () => ( + + + + + + + +); diff --git a/Arc/React/.frontend/index.css b/Arc/React/.frontend/index.css new file mode 100644 index 00000000..2c2e6740 --- /dev/null +++ b/Arc/React/.frontend/index.css @@ -0,0 +1,58 @@ +/* Copyright (c) Cratis. All rights reserved. + * Licensed under the MIT license. See LICENSE file in the project root for full license information. + */ + +:root { + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + color: var(--text-color); + background: var(--surface-ground); + font-synthesis: none; + text-rendering: optimizeLegibility; +} + +* { + box-sizing: border-box; +} + +html { + min-width: 320px; + min-height: 100%; + background: var(--surface-ground); +} + +body { + min-width: 320px; + min-height: 100vh; + margin: 0; + background: + radial-gradient(circle at 86% 7%, color-mix(in srgb, var(--primary-color) 15%, transparent), transparent 32rem), + radial-gradient(circle at 8% 44%, color-mix(in srgb, var(--highlight-bg) 60%, transparent), transparent 36rem), + var(--surface-ground); +} + +button, +input, +textarea { + font: inherit; +} + +button, +a { + -webkit-tap-highlight-color: transparent; +} + +#root { + min-height: 100vh; +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} diff --git a/Arc/React/.frontend/index.html b/Arc/React/.frontend/index.html new file mode 100644 index 00000000..8046e63b --- /dev/null +++ b/Arc/React/.frontend/index.html @@ -0,0 +1,18 @@ + + + + + + + + + Idea Loom · Arc + React + + +
+ + + diff --git a/Arc/React/.frontend/index.tsx b/Arc/React/.frontend/index.tsx new file mode 100644 index 00000000..20c042f6 --- /dev/null +++ b/Arc/React/.frontend/index.tsx @@ -0,0 +1,24 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import '@cratis/components/tokens'; +import '@cratis/components/styles'; +import 'primeicons/primeicons.css'; +import 'primereact/resources/primereact.min.css'; +import 'primereact/resources/themes/lara-light-blue/theme.css'; +import 'reflect-metadata'; +import './index.css'; +import { Bindings } from '@cratis/arc.react.mvvm'; +import { configure as configureMobx } from 'mobx'; +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App'; + +Bindings.initialize(); +configureMobx({ enforceActions: 'never' }); + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/Arc/React/.frontend/tsconfig.json b/Arc/React/.frontend/tsconfig.json new file mode 100644 index 00000000..123d0166 --- /dev/null +++ b/Arc/React/.frontend/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../.frontend/tsconfig.base.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "baseUrl": "..", + "moduleResolution": "bundler", + "types": [ + "chai", + "react", + "react-dom", + "vitest/globals" + ] + }, + "include": [ + "../**/*.ts", + "../**/*.tsx" + ], + "exclude": [ + "vite.config.ts" + ] +} diff --git a/Arc/React/.frontend/tsconfig.node.json b/Arc/React/.frontend/tsconfig.node.json new file mode 100644 index 00000000..bfe32424 --- /dev/null +++ b/Arc/React/.frontend/tsconfig.node.json @@ -0,0 +1,6 @@ +{ + "extends": "../../../.frontend/tsconfig.node.json", + "include": [ + "vite.config.ts" + ] +} diff --git a/Arc/React/.frontend/vite.config.ts b/Arc/React/.frontend/vite.config.ts new file mode 100644 index 00000000..42d536b1 --- /dev/null +++ b/Arc/React/.frontend/vite.config.ts @@ -0,0 +1,51 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { EmitMetadataPlugin } from '@cratis/arc.vite'; +import react from '@vitejs/plugin-react'; +import { fileURLToPath } from 'node:url'; +import type { PluginOption } from 'vite'; +import { defineConfig } from 'vitest/config'; + +const backend = 'http://localhost:5064'; + +export default defineConfig({ + root: fileURLToPath(new URL('./', import.meta.url)), + build: { + outDir: '../wwwroot', + emptyOutDir: true, + assetsDir: 'assets', + target: 'esnext', + modulePreload: false, + chunkSizeWarningLimit: 700, + }, + plugins: [ + react(), + // SAFETY: Arc and this app share Vite's plugin lifecycle; the cast only bridges their bundled Vite type identities. + EmitMetadataPlugin({ + tsconfigPath: fileURLToPath(new URL('./tsconfig.json', import.meta.url)), + }) as unknown as PluginOption, + ], + server: { + host: true, + port: 5173, + open: false, + proxy: { + '/api': { + target: backend, + ws: true, + }, + '/.cratis': { + target: backend, + ws: true, + }, + }, + }, + test: { + globals: true, + environment: 'node', + include: ['../**/for_*/when_*/**/*.ts', '../**/for_*/when_*.ts'], + exclude: ['../wwwroot/**', '../bin/**', '../obj/**', '../node_modules/**'], + setupFiles: fileURLToPath(new URL('../../../.frontend/vitest.setup.ts', import.meta.url)), + }, +}); diff --git a/Arc/React/Arc.React.Specs/Arc.React.Specs.csproj b/Arc/React/Arc.React.Specs/Arc.React.Specs.csproj new file mode 100644 index 00000000..c60d6e0f --- /dev/null +++ b/Arc/React/Arc.React.Specs/Arc.React.Specs.csproj @@ -0,0 +1,26 @@ + + + false + true + Arc.React.Specs + + $(NoWarn);CA1001;IDE1006;SA1134 + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + CratisProxiesOutputPath= + + + diff --git a/Arc/React/Arc.React.Specs/for_IdeaStore/when_capturing_an_idea/with_valid_details.cs b/Arc/React/Arc.React.Specs/for_IdeaStore/when_capturing_an_idea/with_valid_details.cs new file mode 100644 index 00000000..ed463a66 --- /dev/null +++ b/Arc/React/Arc.React.Specs/for_IdeaStore/when_capturing_an_idea/with_valid_details.cs @@ -0,0 +1,21 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Arc.React.Ideas.Board; +using Cratis.Specifications; +using Xunit; + +namespace Arc.React.Specs.for_IdeaStore.when_capturing_an_idea; + +public class with_valid_details : Specification +{ + readonly IdeaStore _store = new(); + readonly IdeaId _ideaId = IdeaId.New(); + + void Because() => new CaptureIdea(_ideaId, "Make setup visible", "Show the shortest path from clone to a running slice.").Handle(_store); + void Destroy() => _store.Dispose(); + + [Fact] void should_add_one_idea() => _store.Current.Count.ShouldEqual(1); + [Fact] void should_keep_the_assigned_identifier() => _store.Current.Single().Id.ShouldEqual(_ideaId); + [Fact] void should_keep_the_title() => _store.Current.Single().Title.ShouldEqual((IdeaTitle)"Make setup visible"); +} diff --git a/Arc/React/Arc.React.csproj b/Arc/React/Arc.React.csproj new file mode 100644 index 00000000..866565f0 --- /dev/null +++ b/Arc/React/Arc.React.csproj @@ -0,0 +1,28 @@ + + + Arc.React + Arc.React + true + true + true + Exe + $(NoWarn);CA1515 + $(MSBuildThisFileDirectory) + 2 + true + true + + + + + + + + + + + + + + + diff --git a/Arc/React/Ideas/Board/Board.cs b/Arc/React/Ideas/Board/Board.cs new file mode 100644 index 00000000..efddf1ef --- /dev/null +++ b/Arc/React/Ideas/Board/Board.cs @@ -0,0 +1,87 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Reactive.Subjects; +using System.Threading; +using Cratis.Arc.Authorization; +using Cratis.Arc.Commands.ModelBound; +using Cratis.Arc.Queries.ModelBound; + +namespace Arc.React.Ideas.Board; + +/// +/// Captures a new idea in the current-state board. +/// +/// The identifier assigned by the client. +/// The concise idea title. +/// The detail that makes the idea useful. +[Command, AllowAnonymous] +public record CaptureIdea(IdeaId Id, IdeaTitle Title, IdeaSummary Summary) +{ + /// + /// Stores the idea directly without appending an event. + /// + /// The current-state idea store. + public void Handle(IdeaStore store) => store.Capture(new(Id, Title, Summary)); +} + +/// +/// Represents an idea shown on the board. +/// +/// The idea identifier. +/// The concise idea title. +/// The useful idea detail. +[ReadModel, AllowAnonymous] +public record Idea(IdeaId Id, IdeaTitle Title, IdeaSummary Summary) +{ + /// + /// Observes all captured ideas and pushes the current board on each change. + /// + /// The current-state idea store. + /// A live sequence containing the current ideas. + public static ISubject> ObserveIdeas(IdeaStore store) => store.Observe(); +} + +/// +/// Holds the current idea board in memory for this focused sample. +/// +public sealed class IdeaStore : IDisposable +{ + readonly Lock _gate = new(); + readonly BehaviorSubject> _ideas = new([]); + + /// + /// Gets a snapshot of the current ideas. + /// + public IReadOnlyList Current + { + get + { + lock (_gate) + { + return [.. _ideas.Value]; + } + } + } + + /// + /// Captures an idea and publishes a fresh board snapshot. + /// + /// The idea to capture. + public void Capture(Idea idea) + { + lock (_gate) + { + _ideas.OnNext([idea, .. _ideas.Value]); + } + } + + /// + /// Observes the current board and every subsequent change. + /// + /// The live idea sequence. + public ISubject> Observe() => _ideas; + + /// + public void Dispose() => _ideas.Dispose(); +} diff --git a/Arc/React/Ideas/Board/Board.css b/Arc/React/Ideas/Board/Board.css new file mode 100644 index 00000000..6b223f14 --- /dev/null +++ b/Arc/React/Ideas/Board/Board.css @@ -0,0 +1,337 @@ +/* Copyright (c) Cratis. All rights reserved. + * Licensed under the MIT license. See LICENSE file in the project root for full license information. + */ + +.idea-loom { + width: min(1180px, calc(100% - 2rem)); + min-height: 100vh; + margin: 0 auto; + padding: 1.25rem 0 2rem; +} + +.app-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.5rem 0 2.75rem; +} + +.brand { + display: inline-flex; + gap: 0.7rem; + align-items: center; + color: var(--text-color); + font-weight: 750; + letter-spacing: -0.02em; + text-decoration: none; +} + +.brand__mark { + display: grid; + width: 2.4rem; + height: 2.4rem; + place-items: center; + color: var(--primary-color-text); + background: var(--primary-color); + border-radius: 0.8rem; + box-shadow: 0 0.6rem 1.6rem color-mix(in srgb, var(--primary-color) 25%, transparent); + font-size: 0.78rem; + letter-spacing: -0.04em; +} + +.architecture-pill { + display: inline-flex; + gap: 0.45rem; + align-items: center; + padding: 0.55rem 0.85rem; + color: var(--text-color-secondary); + background: color-mix(in srgb, var(--surface-card) 78%, transparent); + border: 1px solid var(--surface-border); + border-radius: 999px; + backdrop-filter: blur(14px); + font-size: 0.8rem; + font-weight: 650; +} + +.architecture-pill i { + color: var(--primary-color); +} + +.hero { + display: grid; + grid-template-columns: minmax(0, 1.3fr) minmax(17rem, 0.7fr); + gap: clamp(2rem, 8vw, 8rem); + align-items: end; + padding: clamp(1rem, 4vw, 3.5rem) 0 clamp(3.5rem, 8vw, 6.5rem); +} + +.eyebrow { + display: inline-block; + margin-bottom: 0.8rem; + color: var(--primary-color); + font-size: 0.74rem; + font-weight: 800; + letter-spacing: 0.14em; + text-transform: uppercase; +} + +.hero h1 { + max-width: 12ch; + margin: 0; + color: var(--text-color); + font-size: clamp(3.5rem, 8vw, 7.25rem); + font-weight: 780; + letter-spacing: -0.075em; + line-height: 0.88; +} + +.hero h1 em { + color: var(--primary-color); + font-family: Georgia, "Times New Roman", serif; + font-weight: 500; +} + +.hero__copy > p { + max-width: 38rem; + margin: 1.75rem 0 0; + color: var(--text-color-secondary); + font-size: clamp(1rem, 1.5vw, 1.18rem); + line-height: 1.7; +} + +.hero__metrics { + display: grid; + gap: 0; + overflow: hidden; + background: color-mix(in srgb, var(--surface-card) 88%, transparent); + border: 1px solid var(--surface-border); + border-radius: 1.35rem; + box-shadow: 0 1.5rem 4rem color-mix(in srgb, var(--surface-900) 8%, transparent); + backdrop-filter: blur(18px); +} + +.hero__metrics div { + display: flex; + align-items: baseline; + justify-content: space-between; + padding: 1.15rem 1.35rem; + border-bottom: 1px solid var(--surface-border); +} + +.hero__metrics div:last-child { + border-bottom: 0; +} + +.hero__metrics strong { + color: var(--text-color); + font-size: 1.15rem; +} + +.hero__metrics span { + color: var(--text-color-secondary); + font-size: 0.78rem; +} + +.board-surface { + padding: clamp(1.25rem, 3vw, 2.25rem); + background: color-mix(in srgb, var(--surface-card) 92%, transparent); + border: 1px solid var(--surface-border); + border-radius: 1.6rem; + box-shadow: 0 1.8rem 5rem color-mix(in srgb, var(--surface-900) 9%, transparent); + backdrop-filter: blur(20px); +} + +.board-surface__heading { + display: flex; + gap: 1rem; + align-items: flex-start; + justify-content: space-between; +} + +.board-surface__heading h2 { + margin: 0; + color: var(--text-color); + font-size: clamp(1.65rem, 3vw, 2.4rem); + letter-spacing: -0.045em; +} + +.search-box { + display: flex; + gap: 0.65rem; + align-items: center; + width: min(30rem, 100%); + margin: 1.6rem 0; + padding: 0.75rem 0.95rem; + color: var(--text-color-secondary); + background: var(--surface-ground); + border: 1px solid var(--surface-border); + border-radius: 0.9rem; + transition: border-color 150ms ease, box-shadow 150ms ease; +} + +.search-box:focus-within { + border-color: var(--primary-color); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 14%, transparent); +} + +.search-box input { + width: 100%; + color: var(--text-color); + background: transparent; + border: 0; + outline: 0; +} + +.search-box input::placeholder { + color: var(--text-color-secondary); +} + +.idea-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.85rem; +} + +.idea-card { + display: grid; + grid-template-columns: auto 1fr; + gap: 1rem; + min-height: 10rem; + padding: 1.15rem; + background: var(--surface-card); + border: 1px solid var(--surface-border); + border-radius: 1rem; + transition: border-color 160ms ease, transform 160ms ease, box-shadow 160ms ease; +} + +.idea-card:hover { + border-color: color-mix(in srgb, var(--primary-color) 40%, var(--surface-border)); + box-shadow: 0 0.9rem 2.3rem color-mix(in srgb, var(--surface-900) 7%, transparent); + transform: translateY(-2px); +} + +.idea-card__number { + display: grid; + width: 2.5rem; + height: 2.5rem; + place-items: center; + color: var(--primary-color); + background: var(--highlight-bg); + border-radius: 0.75rem; + font-size: 0.73rem; + font-weight: 800; +} + +.idea-card h3 { + margin: 0.25rem 0 0.6rem; + color: var(--text-color); + font-size: 1.1rem; + letter-spacing: -0.02em; +} + +.idea-card p { + margin: 0; + color: var(--text-color-secondary); + line-height: 1.62; +} + +.empty-state, +.board-state { + display: grid; + min-height: 18rem; + place-items: center; + align-content: center; + padding: 2rem; + color: var(--text-color-secondary); + text-align: center; + background: color-mix(in srgb, var(--surface-ground) 68%, transparent); + border: 1px dashed var(--surface-border); + border-radius: 1rem; +} + +.empty-state__icon { + display: grid; + width: 3.6rem; + height: 3.6rem; + margin-bottom: 1rem; + place-items: center; + color: var(--primary-color); + background: var(--highlight-bg); + border-radius: 1.1rem; + font-size: 1.35rem; +} + +.empty-state h3 { + margin: 0 0 0.45rem; + color: var(--text-color); + font-size: 1.35rem; +} + +.empty-state p { + max-width: 32rem; + margin: 0 0 1.25rem; + line-height: 1.6; +} + +.empty-state button { + padding: 0.75rem 1rem; + color: var(--primary-color-text); + background: var(--primary-color); + border: 0; + border-radius: 0.75rem; + box-shadow: 0 0.7rem 1.7rem color-mix(in srgb, var(--primary-color) 22%, transparent); + cursor: pointer; + font-weight: 700; +} + +.capture-idea__intro { + margin: 0 0 1.1rem; + color: var(--text-color-secondary); + line-height: 1.55; +} + +.idea-loom footer { + display: flex; + flex-wrap: wrap; + gap: 0.65rem 1.4rem; + padding: 1.4rem 0 0; + color: var(--text-color-secondary); + font-size: 0.75rem; +} + +.idea-loom footer span::before { + margin-right: 0.5rem; + color: var(--primary-color); + content: "•"; +} + +@media (max-width: 760px) { + .idea-loom { + width: min(100% - 1rem, 1180px); + padding-top: 0.7rem; + } + + .architecture-pill { + max-width: 11rem; + justify-content: center; + text-align: center; + } + + .hero { + grid-template-columns: 1fr; + gap: 2.5rem; + padding-bottom: 3.5rem; + } + + .hero h1 { + font-size: clamp(3.25rem, 18vw, 5rem); + } + + .board-surface__heading { + flex-direction: column; + } + + .idea-grid { + grid-template-columns: 1fr; + } +} diff --git a/Arc/React/Ideas/Board/Board.tsx b/Arc/React/Ideas/Board/Board.tsx new file mode 100644 index 00000000..f6f0c62c --- /dev/null +++ b/Arc/React/Ideas/Board/Board.tsx @@ -0,0 +1,121 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { useDialog } from '@cratis/arc.react/dialogs'; +import { withViewModel } from '@cratis/arc.react.mvvm'; +import { Toolbar, ToolbarButton } from '@cratis/components/Toolbar'; +import { BoardViewModel } from './BoardViewModel'; +import { CaptureIdeaDialog } from './CaptureIdeaDialog'; +import { Idea } from './Idea'; +import { ObserveIdeas } from './ObserveIdeas'; +import './Board.css'; + +interface IdeaCardProps { + idea: Idea; + sequence: number; +} + +const IdeaCard = ({ idea, sequence }: IdeaCardProps) => ( +
+
{String(sequence).padStart(2, '0')}
+
+

{idea.title}

+

{idea.summary}

+
+
+); + +export const Board = withViewModel(BoardViewModel, ({ viewModel }) => { + const [ideasResult] = ObserveIdeas.use(); + const [CaptureDialog, showCaptureDialog] = useDialog(CaptureIdeaDialog); + const ideas = viewModel.filter(ideasResult.data ?? []); + const capturedCount = ideasResult.data?.length ?? 0; + + return ( +
+
+ + IL + Idea Loom + + Arc CQRS · no Chronicle +
+ +
+
+ A focused Arc + React sample +

Shape the next
small win.

+

+ Capture a useful idea, watch the live query update, and trace one strongly typed contract from C# to React. +

+
+
+
{capturedCount}ideas captured
+
Liveobservable query
+
Directcurrent-state write
+
+
+ +
+
+
+ Working set +

Ideas worth a conversation

+
+ + { void showCaptureDialog(); }} + /> + +
+ +