@askrjs/cli 0.0.11 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -73,8 +73,10 @@ askr analyze --json --check
73
73
 
74
74
  All diagnostics include a stable rule ID and workspace-relative source
75
75
  location. The analyzer distinguishes canonical Askr imports from unrelated
76
- same-named functions and only recommends `<For>` for state-backed reactive JSX
77
- collections, so static transforms with `.map()` remain valid.
76
+ same-named functions and recommends `<For>` only when a `.map()` result is
77
+ rendered directly as JSX children, so ordinary data transforms remain valid.
78
+ It reports eager `<For>`/`<Show>`/`<Case>` controls behind changing ternaries
79
+ while accepting conditionally mounted components with their own render scope.
78
80
 
79
81
  By default it transactionally applies only mechanical route-parameter and
80
82
  plain-JSON JSX configuration fixes. `--check` is read-only for CI. Semantic
package/dist/analyze.js CHANGED
@@ -71,7 +71,7 @@ async function runAnalyzeCli(args = process.argv.slice(2), io = console, runtime
71
71
  io.log(helpText.trimEnd());
72
72
  return 0;
73
73
  }
74
- const report = await (runtime.analyze ?? (await import("./runner-42nqhW9u.js")).runAnalysis)({
74
+ const report = await (runtime.analyze ?? (await import("./runner-BRjjPhZY.js")).runAnalysis)({
75
75
  cwd: parsed.cwd,
76
76
  workspacePatterns: parsed.workspacePatterns,
77
77
  check: parsed.check
package/dist/cli.js CHANGED
@@ -90,7 +90,7 @@ async function runCli(args = process.argv.slice(2), io = console) {
90
90
  return runAnalyzeCli(args.slice(1), io);
91
91
  }
92
92
  if (command === "check" || command === "doctor" || command === "repair") {
93
- const { runGuardrailCli } = await import("./guardrails-vH9lYg05.js");
93
+ const { runGuardrailCli } = await import("./guardrails-GPM9PJqO.js");
94
94
  return runGuardrailCli(command, args.slice(1), io);
95
95
  }
96
96
  if (command === "generate") {
@@ -98,7 +98,7 @@ async function runGuardrailCli(command, args = process.argv.slice(2), io = conso
98
98
  cwd: parsed.cwd,
99
99
  workspacePatterns: parsed.workspacePatterns
100
100
  };
101
- const { runCheck, runDoctor, runRepair } = await import("./runner-tPGn2CQC.js");
101
+ const { runCheck, runDoctor, runRepair } = await import("./runner-Ca43qqzG.js");
102
102
  const report = command === "doctor" ? await runDoctor(options, runtime) : command === "repair" ? await runRepair(options) : await runCheck(options, runtime);
103
103
  if (parsed.json) io.log(JSON.stringify(report));
104
104
  else if (report.command === "doctor") printDoctor(report, io);
@@ -354,6 +354,30 @@ function functionName(node) {
354
354
  }
355
355
  return null;
356
356
  }
357
+ const EAGER_CONTROL_CONCEPTS = /* @__PURE__ */ new Set([
358
+ "Case",
359
+ "For",
360
+ "Show"
361
+ ]);
362
+ function isConstantCondition(expression) {
363
+ if (ts.isParenthesizedExpression(expression)) return isConstantCondition(expression.expression);
364
+ return expression.kind === ts.SyntaxKind.TrueKeyword || expression.kind === ts.SyntaxKind.FalseKeyword || expression.kind === ts.SyntaxKind.NullKeyword || ts.isNumericLiteral(expression) || ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression);
365
+ }
366
+ function unstableControlConditional(node, bindings) {
367
+ const ownElement = ts.isJsxOpeningElement(node) && ts.isJsxElement(node.parent) ? node.parent : null;
368
+ let child = node;
369
+ for (let current = node.parent; current; current = current.parent) {
370
+ if (ts.isFunctionLike(current)) return null;
371
+ if (current !== ownElement && ts.isJsxElement(current)) {
372
+ const name = canonicalJsxName(current.openingElement.tagName, bindings);
373
+ if (name && EAGER_CONTROL_CONCEPTS.has(name)) return null;
374
+ }
375
+ if (ts.isConditionalExpression(current) && (current.whenTrue === child || current.whenFalse === child) && !isConstantCondition(current.condition)) return current;
376
+ if (ts.isBinaryExpression(current) && [ts.SyntaxKind.AmpersandAmpersandToken, ts.SyntaxKind.BarBarToken].includes(current.operatorToken.kind) && current.right === child && !isConstantCondition(current.left)) return current;
377
+ child = current;
378
+ }
379
+ return null;
380
+ }
357
381
  const stableRenderRule = {
358
382
  id: "askr/stable-render-call",
359
383
  category: "correctness",
@@ -375,6 +399,14 @@ const stableRenderRule = {
375
399
  }
376
400
  if (isControlFlowAncestor(node, owner) && (!POSITIONAL_DATA_CONCEPTS.has(name) || /^[A-Z]/.test(functionName(owner) ?? "") || containsJsx(owner))) diagnostics.push(diagnostic(context, node.expression, this, `${name}() is called conditionally, so its render position is unstable.`, `Call ${name}() unconditionally at the top level and branch on its result.`));
377
401
  });
402
+ visit(sourceFile, (node) => {
403
+ if (!ts.isJsxOpeningElement(node) && !ts.isJsxSelfClosingElement(node)) return;
404
+ const name = canonicalJsxName(node.tagName, bindings);
405
+ if (!name || !EAGER_CONTROL_CONCEPTS.has(name)) return;
406
+ if (!unstableControlConditional(node, bindings)) return;
407
+ const remediation = name === "Show" ? "Mount <Show> unconditionally and move the condition into its when prop." : "Replace the ternary or logical expression with a <Show> boundary.";
408
+ diagnostics.push(diagnostic(context, node.tagName, this, `<${name}> is mounted behind a changing conditional, so its render position is unstable.`, remediation));
409
+ });
378
410
  }
379
411
  return diagnostics;
380
412
  }
@@ -594,26 +626,37 @@ const stableKeyRule = {
594
626
  return diagnostics;
595
627
  }
596
628
  };
597
- function reactiveMapReceiver(expression, stateGetters) {
598
- if (ts.isCallExpression(expression) && ts.isIdentifier(expression.expression)) return stateGetters.has(expression.expression.text);
599
- if (ts.isCallExpression(expression) && ts.isPropertyAccessExpression(expression.expression)) return reactiveMapReceiver(expression.expression.expression, stateGetters);
600
- if (ts.isPropertyAccessExpression(expression)) return reactiveMapReceiver(expression.expression, stateGetters);
601
- return false;
629
+ function mapResultIsJsxChild(node) {
630
+ let current = node;
631
+ for (;;) {
632
+ const parent = current.parent;
633
+ if (ts.isJsxExpression(parent)) return !ts.isJsxAttribute(parent.parent);
634
+ if ((ts.isParenthesizedExpression(parent) || ts.isAsExpression(parent) || ts.isTypeAssertionExpression(parent) || ts.isNonNullExpression(parent) || ts.isSatisfiesExpression(parent)) && parent.expression === current) {
635
+ current = parent;
636
+ continue;
637
+ }
638
+ if (ts.isConditionalExpression(parent) && (parent.whenTrue === current || parent.whenFalse === current)) {
639
+ current = parent;
640
+ continue;
641
+ }
642
+ if (ts.isBinaryExpression(parent) && (parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken && parent.right === current || parent.operatorToken.kind === ts.SyntaxKind.BarBarToken && (parent.left === current || parent.right === current) || parent.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken && (parent.left === current || parent.right === current))) {
643
+ current = parent;
644
+ continue;
645
+ }
646
+ return false;
647
+ }
602
648
  }
603
649
  const preferForRule = {
604
650
  id: "askr/prefer-for",
605
651
  category: "performance",
606
652
  severity: "warning",
607
- description: "Reactive JSX collections should use For for keyed reconciliation.",
653
+ description: "Collection arrays rendered as JSX children should use For.",
608
654
  analyze(context) {
609
655
  const diagnostics = [];
610
- for (const sourceFile of context.sourceFiles) {
611
- const state = collectStateBindings(sourceFile, sourceBindings(sourceFile));
612
- visit(sourceFile, (node) => {
613
- if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression) || node.expression.name.text !== "map" || !reactiveMapReceiver(node.expression.expression, state.getters) || !node.parent || !ts.isJsxExpression(node.parent)) return;
614
- diagnostics.push(diagnostic(context, node.expression.name, this, "A reactive collection is rendered with .map(), bypassing keyed <For> reconciliation.", "Render it with <For each={...} by={...}>. This semantic rewrite is report-only."));
615
- });
616
- }
656
+ for (const sourceFile of context.sourceFiles) visit(sourceFile, (node) => {
657
+ if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression) || node.expression.name.text !== "map" || !mapResultIsJsxChild(node)) return;
658
+ diagnostics.push(diagnostic(context, node.expression.name, this, "A collection array is rendered with .map(), bypassing keyed <For> reconciliation.", "Render it with <For each={...} by={...}>. This semantic rewrite is report-only."));
659
+ });
617
660
  return diagnostics;
618
661
  }
619
662
  };
@@ -1,6 +1,6 @@
1
1
  import { t as inspectBundledSkills } from "./skills-CSGdAZHN.js";
2
2
  import { discoverWorkspaceProject } from "./discovery-DUDrZCIC.js";
3
- import { analysisHasBlockingFindings, runAnalysis } from "./runner-42nqhW9u.js";
3
+ import { analysisHasBlockingFindings, runAnalysis } from "./runner-BRjjPhZY.js";
4
4
  import fs from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { spawn } from "node:child_process";
@@ -6,6 +6,7 @@ import {
6
6
  SettingsIcon,
7
7
  SunIcon,
8
8
  } from '@askrjs/lucide';
9
+ import { For } from '@askrjs/askr/control';
9
10
  import { Link, navigate } from '@askrjs/askr/router';
10
11
  import { Button } from '@askrjs/themes/components';
11
12
  import { Container, Inline, Stack } from '@askrjs/themes/components';
@@ -42,14 +43,16 @@ export default function AppLayout({ children }: { children?: unknown }) {
42
43
  </Link>
43
44
  </NavBrand>
44
45
  <NavGroup label="Workspace">
45
- {appNavItems.map((item) => (
46
- <NavLink href={item.href} match={item.match}>
47
- <Inline as="span" gap="2" align="center">
48
- {icons[item.icon]}
49
- <span>{item.label}</span>
50
- </Inline>
51
- </NavLink>
52
- ))}
46
+ <For each={[...appNavItems]} by={(item) => item.href}>
47
+ {(item) => (
48
+ <NavLink href={item.href} match={item.match}>
49
+ <Inline as="span" gap="2" align="center">
50
+ {icons[item.icon]}
51
+ <span>{item.label}</span>
52
+ </Inline>
53
+ </NavLink>
54
+ )}
55
+ </For>
53
56
  </NavGroup>
54
57
  <NavGroup label="Session" align="end">
55
58
  <NavLink href="/" match="exact">
@@ -1,4 +1,5 @@
1
1
  import { resource } from '@askrjs/askr/resources';
2
+ import { For, Show } from '@askrjs/askr/control';
2
3
  import { createPlot } from '@askrjs/charts';
3
4
  import { AlertCircleIcon, RefreshCwIcon } from '@askrjs/lucide';
4
5
  import { Button } from '@askrjs/themes/components';
@@ -66,105 +67,111 @@ export default function AdminHomePage() {
66
67
  </Block>
67
68
  ) : null}
68
69
 
69
- {snapshot ? (
70
- <>
71
- <Block gap="md" class="metric-grid">
72
- {snapshot.metrics.map((metric) => (
73
- <MetricCard
74
- label={metric.label}
75
- value={metric.value}
76
- trend={metric.trend}
77
- />
78
- ))}
79
- </Block>
70
+ <Show when={snapshot}>
71
+ {(currentSnapshot) => (
72
+ <>
73
+ <Block gap="md" class="metric-grid">
74
+ <For each={currentSnapshot.metrics} by={(metric) => metric.label}>
75
+ {(metric) => (
76
+ <MetricCard
77
+ label={metric.label}
78
+ value={metric.value}
79
+ trend={metric.trend}
80
+ />
81
+ )}
82
+ </For>
83
+ </Block>
84
+
85
+ <Block gap="md" align="stretch" class="chart-grid">
86
+ <Card>
87
+ <CardHeader>
88
+ <CardTitle>Run throughput</CardTitle>
89
+ <CardDescription>
90
+ Accepted commands by work type.
91
+ </CardDescription>
92
+ </CardHeader>
93
+ <CardContent>
94
+ <OperationsPlot.Root
95
+ data={currentSnapshot.throughput}
96
+ rowKey="label"
97
+ label="Run throughput"
98
+ description="Accepted commands by work type."
99
+ >
100
+ <OperationsPlot.Bar x="label" y="value" />
101
+ </OperationsPlot.Root>
102
+ </CardContent>
103
+ </Card>
104
+ <Card>
105
+ <CardHeader>
106
+ <CardTitle>Projection lag</CardTitle>
107
+ <CardDescription>
108
+ Lower is better; stale states stay visible.
109
+ </CardDescription>
110
+ </CardHeader>
111
+ <CardContent>
112
+ <OperationsPlot.Root
113
+ data={currentSnapshot.lag}
114
+ rowKey="label"
115
+ label="Projection lag"
116
+ description="Projection lag over the last hour."
117
+ >
118
+ <OperationsPlot.Line x="label" y="value" />
119
+ <OperationsPlot.Point x="label" y="value" />
120
+ </OperationsPlot.Root>
121
+ </CardContent>
122
+ </Card>
123
+ </Block>
124
+
125
+ {currentSnapshot.consistency !== 'fresh' ? (
126
+ <Alert variant="warning">
127
+ Read models are {currentSnapshot.consistency}. Last processed
128
+ event is {currentSnapshot.lastEventId}.
129
+ </Alert>
130
+ ) : null}
80
131
 
81
- <Block gap="md" align="stretch" class="chart-grid">
82
- <Card>
83
- <CardHeader>
84
- <CardTitle>Run throughput</CardTitle>
85
- <CardDescription>
86
- Accepted commands by work type.
87
- </CardDescription>
88
- </CardHeader>
89
- <CardContent>
90
- <OperationsPlot.Root
91
- data={snapshot.throughput}
92
- rowKey="label"
93
- label="Run throughput"
94
- description="Accepted commands by work type."
95
- >
96
- <OperationsPlot.Bar x="label" y="value" />
97
- </OperationsPlot.Root>
98
- </CardContent>
99
- </Card>
100
132
  <Card>
101
133
  <CardHeader>
102
- <CardTitle>Projection lag</CardTitle>
134
+ <CardTitle>Recent agent runs</CardTitle>
103
135
  <CardDescription>
104
- Lower is better; stale states stay visible.
136
+ Run state is modeled as product state, not a single loading
137
+ boolean.
105
138
  </CardDescription>
106
139
  </CardHeader>
107
140
  <CardContent>
108
- <OperationsPlot.Root
109
- data={snapshot.lag}
110
- rowKey="label"
111
- label="Projection lag"
112
- description="Projection lag over the last hour."
113
- >
114
- <OperationsPlot.Line x="label" y="value" />
115
- <OperationsPlot.Point x="label" y="value" />
116
- </OperationsPlot.Root>
117
- </CardContent>
118
- </Card>
119
- </Block>
120
-
121
- {snapshot.consistency !== 'fresh' ? (
122
- <Alert variant="warning">
123
- Read models are {snapshot.consistency}. Last processed event is{' '}
124
- {snapshot.lastEventId}.
125
- </Alert>
126
- ) : null}
127
-
128
- <Card>
129
- <CardHeader>
130
- <CardTitle>Recent agent runs</CardTitle>
131
- <CardDescription>
132
- Run state is modeled as product state, not a single loading
133
- boolean.
134
- </CardDescription>
135
- </CardHeader>
136
- <CardContent>
137
- <div class="run-table-wrap">
138
- <table class="run-table">
139
- <thead>
140
- <tr>
141
- <th>Run</th>
142
- <th>Status</th>
143
- <th>Owner</th>
144
- <th>Updated</th>
145
- </tr>
146
- </thead>
147
- <tbody>
148
- {snapshot.runs.map((run) => (
141
+ <div class="run-table-wrap">
142
+ <table class="run-table">
143
+ <thead>
149
144
  <tr>
150
- <td>
151
- <strong>{run.title}</strong>
152
- <span>{run.id}</span>
153
- </td>
154
- <td>
155
- <StatusBadge status={run.status} />
156
- </td>
157
- <td>{run.owner}</td>
158
- <td>{formatRelativeTime(run.updatedAt)}</td>
145
+ <th>Run</th>
146
+ <th>Status</th>
147
+ <th>Owner</th>
148
+ <th>Updated</th>
159
149
  </tr>
160
- ))}
161
- </tbody>
162
- </table>
163
- </div>
164
- </CardContent>
165
- </Card>
166
- </>
167
- ) : null}
150
+ </thead>
151
+ <tbody>
152
+ <For each={currentSnapshot.runs} by={(run) => run.id}>
153
+ {(run) => (
154
+ <tr>
155
+ <td>
156
+ <strong>{run.title}</strong>
157
+ <span>{run.id}</span>
158
+ </td>
159
+ <td>
160
+ <StatusBadge status={run.status} />
161
+ </td>
162
+ <td>{run.owner}</td>
163
+ <td>{formatRelativeTime(run.updatedAt)}</td>
164
+ </tr>
165
+ )}
166
+ </For>
167
+ </tbody>
168
+ </table>
169
+ </div>
170
+ </CardContent>
171
+ </Card>
172
+ </>
173
+ )}
174
+ </Show>
168
175
  </Stack>
169
176
  );
170
177
  }
@@ -4,6 +4,7 @@ import {
4
4
  Clock3Icon,
5
5
  ShieldAlertIcon,
6
6
  } from '@askrjs/lucide';
7
+ import { For } from '@askrjs/askr/control';
7
8
  import {
8
9
  Badge,
9
10
  Card,
@@ -18,12 +19,14 @@ import StatusBadge, {
18
19
  } from '../../components/shared/status-badge';
19
20
 
20
21
  const runs: Array<{
22
+ id: string;
21
23
  title: string;
22
24
  status: RunStatus;
23
25
  event: string;
24
26
  description: string;
25
27
  }> = [
26
28
  {
29
+ id: 'reconcile-billing-projection',
27
30
  title: 'Reconcile billing projection',
28
31
  status: 'running',
29
32
  event: 'tool call: compare-ledger',
@@ -31,6 +34,7 @@ const runs: Array<{
31
34
  'Streaming events are appended to the timeline and reconciled by event id.',
32
35
  },
33
36
  {
37
+ id: 'approve-enterprise-workspace',
34
38
  title: 'Approve enterprise workspace',
35
39
  status: 'requires-action',
36
40
  event: 'approval requested',
@@ -38,6 +42,7 @@ const runs: Array<{
38
42
  'Human gates are explicit product states, not hidden inside generated text.',
39
43
  },
40
44
  {
45
+ id: 'refresh-onboarding-cohort',
41
46
  title: 'Refresh onboarding cohort',
42
47
  status: 'succeeded',
43
48
  event: 'projection caught up',
@@ -61,32 +66,34 @@ export default function AgentRunsPage() {
61
66
  </section>
62
67
 
63
68
  <Block gap="md" class="agent-grid">
64
- {runs.map((run) => (
65
- <Card>
66
- <CardHeader>
67
- <Inline justify="between" align="start" gap="3">
68
- <span class="card-icon">
69
- {run.status === 'succeeded' ? (
70
- <CheckCircle2Icon size={18} aria-hidden="true" />
71
- ) : run.status === 'requires-action' ? (
72
- <ShieldAlertIcon size={18} aria-hidden="true" />
73
- ) : (
74
- <BotIcon size={18} aria-hidden="true" />
75
- )}
76
- </span>
77
- <StatusBadge status={run.status} />
78
- </Inline>
79
- <CardTitle>{run.title}</CardTitle>
80
- <CardDescription>{run.description}</CardDescription>
81
- </CardHeader>
82
- <CardContent>
83
- <Inline gap="2" align="center">
84
- <Clock3Icon size={14} aria-hidden="true" />
85
- <span>{run.event}</span>
86
- </Inline>
87
- </CardContent>
88
- </Card>
89
- ))}
69
+ <For each={runs} by={(run) => run.id}>
70
+ {(run) => (
71
+ <Card>
72
+ <CardHeader>
73
+ <Inline justify="between" align="start" gap="3">
74
+ <span class="card-icon">
75
+ {run.status === 'succeeded' ? (
76
+ <CheckCircle2Icon size={18} aria-hidden="true" />
77
+ ) : run.status === 'requires-action' ? (
78
+ <ShieldAlertIcon size={18} aria-hidden="true" />
79
+ ) : (
80
+ <BotIcon size={18} aria-hidden="true" />
81
+ )}
82
+ </span>
83
+ <StatusBadge status={run.status} />
84
+ </Inline>
85
+ <CardTitle>{run.title}</CardTitle>
86
+ <CardDescription>{run.description}</CardDescription>
87
+ </CardHeader>
88
+ <CardContent>
89
+ <Inline gap="2" align="center">
90
+ <Clock3Icon size={14} aria-hidden="true" />
91
+ <span>{run.event}</span>
92
+ </Inline>
93
+ </CardContent>
94
+ </Card>
95
+ )}
96
+ </For>
90
97
  </Block>
91
98
  </Stack>
92
99
  );
@@ -5,6 +5,7 @@ import {
5
5
  CheckCircle2Icon,
6
6
  ShieldCheckIcon,
7
7
  } from '@askrjs/lucide';
8
+ import { For } from '@askrjs/askr/control';
8
9
  import { Link } from '@askrjs/askr/router';
9
10
  import { Button } from '@askrjs/themes/components';
10
11
  import {
@@ -111,15 +112,17 @@ export default function HomePage() {
111
112
  <Section paddingY="xl">
112
113
  <Container size="xl">
113
114
  <Block gap="md" class="feature-grid">
114
- {capabilities.map((item) => (
115
- <Card>
116
- <CardHeader>
117
- <span class="card-icon">{item.icon}</span>
118
- <CardTitle>{item.title}</CardTitle>
119
- <CardDescription>{item.description}</CardDescription>
120
- </CardHeader>
121
- </Card>
122
- ))}
115
+ <For each={capabilities} by={(item) => item.title}>
116
+ {(item) => (
117
+ <Card>
118
+ <CardHeader>
119
+ <span class="card-icon">{item.icon}</span>
120
+ <CardTitle>{item.title}</CardTitle>
121
+ <CardDescription>{item.description}</CardDescription>
122
+ </CardHeader>
123
+ </Card>
124
+ )}
125
+ </For>
123
126
  </Block>
124
127
  </Container>
125
128
  </Section>
@@ -1,3 +1,4 @@
1
+ import { For } from '@askrjs/askr/control';
1
2
  import { Link } from '@askrjs/askr/router';
2
3
  import { Header } from '@askrjs/themes/components';
3
4
  import {
@@ -35,9 +36,9 @@ export function SiteHeader() {
35
36
  class="navbar-group"
36
37
  data-align="end"
37
38
  >
38
- {navItems.map((item) => (
39
- <NavLink href={item.href}>{item.label}</NavLink>
40
- ))}
39
+ <For each={[...navItems]} by={(item) => item.href}>
40
+ {(item) => <NavLink href={item.href}>{item.label}</NavLink>}
41
+ </For>
41
42
  </Nav>
42
43
  </Box>
43
44
  </Container>
@@ -1,3 +1,4 @@
1
+ import { For } from '@askrjs/askr/control';
1
2
  import { Link } from '@askrjs/askr/router';
2
3
  import { Button } from '@askrjs/ui';
3
4
  import {
@@ -54,17 +55,19 @@ export default function Workflow() {
54
55
  />
55
56
 
56
57
  <CardGrid>
57
- {steps.map((step) => (
58
- <Card
59
- eyebrow={step.number}
60
- title={step.title}
61
- description={step.body}
62
- >
63
- <p>
64
- <code>{step.command}</code>
65
- </p>
66
- </Card>
67
- ))}
58
+ <For each={steps} by={(step) => step.number}>
59
+ {(step) => (
60
+ <Card
61
+ eyebrow={step.number}
62
+ title={step.title}
63
+ description={step.body}
64
+ >
65
+ <p>
66
+ <code>{step.command}</code>
67
+ </p>
68
+ </Card>
69
+ )}
70
+ </For>
68
71
  </CardGrid>
69
72
 
70
73
  <Card
@@ -1,3 +1,4 @@
1
+ import { For } from '@askrjs/askr/control';
1
2
  import { Link } from '@askrjs/askr/router';
2
3
  import { Button } from '@askrjs/ui';
3
4
  import {
@@ -50,13 +51,15 @@ export default function Content() {
50
51
  />
51
52
 
52
53
  <CardGrid>
53
- {routeMap.map((route) => (
54
- <Card
55
- eyebrow={route.path}
56
- title={route.title}
57
- description={route.note}
58
- />
59
- ))}
54
+ <For each={[...routeMap]} by={(route) => route.path}>
55
+ {(route) => (
56
+ <Card
57
+ eyebrow={route.path}
58
+ title={route.title}
59
+ description={route.note}
60
+ />
61
+ )}
62
+ </For>
60
63
  </CardGrid>
61
64
  </>
62
65
  );
@@ -1,3 +1,4 @@
1
+ import { For } from '@askrjs/askr/control';
1
2
  import { Link } from '@askrjs/askr/router';
2
3
  import { Button } from '@askrjs/ui';
3
4
  import {
@@ -42,9 +43,11 @@ export default function Home() {
42
43
  />
43
44
 
44
45
  <CardGrid>
45
- {highlights.map((highlight) => (
46
- <Card title={highlight.title} description={highlight.body} />
47
- ))}
46
+ <For each={highlights} by={(highlight) => highlight.title}>
47
+ {(highlight) => (
48
+ <Card title={highlight.title} description={highlight.body} />
49
+ )}
50
+ </For>
48
51
  </CardGrid>
49
52
  </>
50
53
  );
@@ -1,3 +1,4 @@
1
+ import { For } from '@askrjs/askr/control';
1
2
  import { Link } from '@askrjs/askr/router';
2
3
  import {
3
4
  LayoutDashboardIcon,
@@ -67,27 +68,31 @@ export default function AppSidebar() {
67
68
  </NavBrand>
68
69
 
69
70
  <NavGroup id="workspace-nav-group" label="Workspace">
70
- {primaryNav.map((item) => {
71
- const Icon = item.icon;
72
- return (
73
- <NavLink href={item.href}>
74
- <Icon size={16} aria-hidden={true} />
75
- <span>{item.label}</span>
76
- </NavLink>
77
- );
78
- })}
71
+ <For each={primaryNav} by={(item) => item.href}>
72
+ {(item) => {
73
+ const Icon = item.icon;
74
+ return (
75
+ <NavLink href={item.href}>
76
+ <Icon size={16} aria-hidden={true} />
77
+ <span>{item.label}</span>
78
+ </NavLink>
79
+ );
80
+ }}
81
+ </For>
79
82
  </NavGroup>
80
83
 
81
84
  <NavGroup id="other-nav-group" label="Other" placement="bottom">
82
- {secondaryNav.map((item) => {
83
- const Icon = item.icon;
84
- return (
85
- <NavLink href={item.href}>
86
- <Icon size={16} aria-hidden={true} />
87
- <span>{item.label}</span>
88
- </NavLink>
89
- );
90
- })}
85
+ <For each={secondaryNav} by={(item) => item.href}>
86
+ {(item) => {
87
+ const Icon = item.icon;
88
+ return (
89
+ <NavLink href={item.href}>
90
+ <Icon size={16} aria-hidden={true} />
91
+ <span>{item.label}</span>
92
+ </NavLink>
93
+ );
94
+ }}
95
+ </For>
91
96
  </NavGroup>
92
97
  </Navbar>
93
98
  </aside>
@@ -58,18 +58,20 @@ export default function DataTable<Row>(props: {
58
58
  <table class={props.tableClass}>
59
59
  <thead>
60
60
  <tr>
61
- {props.columns.map((column) => (
62
- <th class={column.class}>{column.header}</th>
63
- ))}
61
+ <For each={props.columns} by={(column) => column.key}>
62
+ {(column) => <th class={column.class}>{column.header}</th>}
63
+ </For>
64
64
  </tr>
65
65
  </thead>
66
66
  <tbody>
67
67
  <For each={props.rows} by={props.rowKey}>
68
68
  {(row: Row) => (
69
69
  <tr class={props.rowClass?.(row)}>
70
- {props.columns.map((column) => (
71
- <td class={column.class}>{column.render(row)}</td>
72
- ))}
70
+ <For each={props.columns} by={(column) => column.key}>
71
+ {(column) => (
72
+ <td class={column.class}>{column.render(row)}</td>
73
+ )}
74
+ </For>
73
75
  </tr>
74
76
  )}
75
77
  </For>
@@ -1,4 +1,5 @@
1
1
  import { state } from '@askrjs/askr';
2
+ import { For } from '@askrjs/askr/control';
2
3
  import { resource } from '@askrjs/askr/resources';
3
4
  import { Button } from '@askrjs/ui/button';
4
5
  import {
@@ -61,19 +62,21 @@ export default function DashboardPage() {
61
62
  />
62
63
 
63
64
  <div class="stat-grid">
64
- {stats().map((stat) => {
65
- const Icon =
66
- iconByStatKey[stat.key as keyof typeof iconByStatKey] ??
67
- BarChart3Icon;
68
- return (
69
- <StatCard
70
- label={stat.label}
71
- value={stat.value}
72
- trend={stat.trend}
73
- icon={Icon}
74
- />
75
- );
76
- })}
65
+ <For each={stats} by={(stat) => stat.key}>
66
+ {(stat) => {
67
+ const Icon =
68
+ iconByStatKey[stat.key as keyof typeof iconByStatKey] ??
69
+ BarChart3Icon;
70
+ return (
71
+ <StatCard
72
+ label={stat.label}
73
+ value={stat.value}
74
+ trend={stat.trend}
75
+ icon={Icon}
76
+ />
77
+ );
78
+ }}
79
+ </For>
77
80
  </div>
78
81
 
79
82
  <section class="panel stack-md">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askrjs/cli",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
4
4
  "description": "Unified CLI for the Askr platform",
5
5
  "homepage": "https://github.com/askrjs/askr-cli#readme",
6
6
  "bugs": {