lucent-ui 0.39.0 → 0.40.0

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.
@@ -3,6 +3,33 @@ import { createServer } from 'node:http';
3
3
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
4
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
5
5
  import { registerTools, DESIGN_RULES_SUMMARY } from './tools.js';
6
+ import { getRecentCalls, subscribeToCalls } from './logger.js';
7
+ function computeAggregates(calls) {
8
+ const perTool = {};
9
+ const keySet = new Set();
10
+ let errors = 0;
11
+ let totalDuration = 0;
12
+ let lastMinute = 0;
13
+ const oneMinuteAgo = Date.now() - 60_000;
14
+ for (const call of calls) {
15
+ perTool[call.tool] = (perTool[call.tool] ?? 0) + 1;
16
+ if (call.key !== undefined)
17
+ keySet.add(call.key);
18
+ if (!call.ok)
19
+ errors += 1;
20
+ totalDuration += call.durationMs;
21
+ if (Date.parse(call.t) >= oneMinuteAgo)
22
+ lastMinute += 1;
23
+ }
24
+ return {
25
+ total: calls.length,
26
+ perTool,
27
+ uniqueKeys: keySet.size,
28
+ lastMinute,
29
+ errors,
30
+ avgDurationMs: calls.length > 0 ? Math.round(totalDuration / calls.length) : null,
31
+ };
32
+ }
6
33
  const PORT = Number(process.env['PORT'] ?? 3000);
7
34
  const HOST = process.env['HOST'] ?? '127.0.0.1';
8
35
  const API_KEY = process.env['LUCENT_API_KEY'];
@@ -73,6 +100,48 @@ const httpServer = createServer(async (req, res) => {
73
100
  writeJson(res, 200, { status: 'ok' });
74
101
  return;
75
102
  }
103
+ // Usage snapshot — recent calls + aggregates for the dashboard.
104
+ if (url.pathname === '/usage' && req.method === 'GET') {
105
+ if (!checkAuth(req)) {
106
+ res.setHeader('WWW-Authenticate', 'Bearer');
107
+ writeJson(res, 401, { error: 'Unauthorized' });
108
+ return;
109
+ }
110
+ const calls = getRecentCalls();
111
+ writeJson(res, 200, {
112
+ calls,
113
+ aggregates: computeAggregates(calls),
114
+ });
115
+ return;
116
+ }
117
+ // Usage live stream — SSE, pushes each new tool call as it happens.
118
+ if (url.pathname === '/usage/stream' && req.method === 'GET') {
119
+ if (!checkAuth(req)) {
120
+ res.setHeader('WWW-Authenticate', 'Bearer');
121
+ writeJson(res, 401, { error: 'Unauthorized' });
122
+ return;
123
+ }
124
+ res.writeHead(200, {
125
+ 'Content-Type': 'text/event-stream',
126
+ 'Cache-Control': 'no-cache',
127
+ Connection: 'keep-alive',
128
+ 'X-Accel-Buffering': 'no', // disable proxy buffering (nginx, some CDNs)
129
+ });
130
+ // Prime the connection so clients know we're live.
131
+ res.write(': connected\n\n');
132
+ const unsubscribe = subscribeToCalls((entry) => {
133
+ res.write(`data: ${JSON.stringify(entry)}\n\n`);
134
+ });
135
+ // Keep-alive heartbeat every 25s so idle connections aren't dropped.
136
+ const heartbeat = setInterval(() => {
137
+ res.write(': heartbeat\n\n');
138
+ }, 25_000);
139
+ req.on('close', () => {
140
+ clearInterval(heartbeat);
141
+ unsubscribe();
142
+ });
143
+ return;
144
+ }
76
145
  if (url.pathname !== MCP_PATH) {
77
146
  writeJson(res, 404, { error: 'Not found' });
78
147
  return;
@@ -1,14 +1,19 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  /**
3
- * Structured logging for MCP tool calls.
3
+ * Structured logging + in-memory ring buffer for MCP tool calls.
4
4
  *
5
5
  * One JSON line per call is written to stderr (greppable, parseable by log
6
- * shippers). Set `LUCENT_MCP_QUIET=1` to disable all logging.
6
+ * shippers), and the same entry is pushed to an in-memory ring buffer used
7
+ * by the `/usage` and `/usage/stream` HTTP endpoints.
7
8
  *
8
- * When `LUCENT_API_KEY` is set, a short hash prefix of the key is included
9
+ * Set `LUCENT_MCP_QUIET=1` to silence stderr the ring buffer still fills,
10
+ * so the usage dashboard keeps working.
11
+ *
12
+ * When `LUCENT_API_KEY` is set, a short sha256 prefix of the key is included
9
13
  * in log entries for usage analytics. The raw key is never logged.
10
14
  */
11
15
  const QUIET = process.env['LUCENT_MCP_QUIET'] === '1';
16
+ const BUFFER_SIZE = 500;
12
17
  /**
13
18
  * Returns the first 8 hex chars of sha256(key). Short enough to stay readable
14
19
  * in logs, safe to leak (pre-image resistant), and unique enough to distinguish
@@ -17,11 +22,27 @@ const QUIET = process.env['LUCENT_MCP_QUIET'] === '1';
17
22
  function hashKeyPrefix(key) {
18
23
  return createHash('sha256').update(key).digest('hex').slice(0, 8);
19
24
  }
25
+ // ─── Ring buffer + subscribers ───────────────────────────────────────────────
26
+ const recentCalls = [];
27
+ const subscribers = new Set();
28
+ /** Snapshot of the ring buffer, newest first. */
29
+ export function getRecentCalls() {
30
+ return recentCalls.slice().reverse();
31
+ }
32
+ /**
33
+ * Subscribe to new tool calls as they happen. Returns an unsubscribe function.
34
+ * Used by the `/usage/stream` SSE endpoint to push live updates.
35
+ */
36
+ export function subscribeToCalls(fn) {
37
+ subscribers.add(fn);
38
+ return () => {
39
+ subscribers.delete(fn);
40
+ };
41
+ }
42
+ // ─── Logging ─────────────────────────────────────────────────────────────────
20
43
  export function logToolCall(entry) {
21
- if (QUIET)
22
- return;
23
44
  const apiKey = process.env['LUCENT_API_KEY'];
24
- const line = JSON.stringify({
45
+ const stored = {
25
46
  t: new Date().toISOString(),
26
47
  tool: entry.tool,
27
48
  params: entry.params,
@@ -29,8 +50,24 @@ export function logToolCall(entry) {
29
50
  ok: entry.ok,
30
51
  ...(entry.error !== undefined && { error: entry.error }),
31
52
  ...(apiKey !== undefined && { key: hashKeyPrefix(apiKey) }),
32
- });
33
- process.stderr.write(line + '\n');
53
+ };
54
+ // 1. Push to ring buffer (always, even in QUIET mode — dashboards still work).
55
+ recentCalls.push(stored);
56
+ if (recentCalls.length > BUFFER_SIZE)
57
+ recentCalls.shift();
58
+ // 2. Notify subscribers (SSE streams).
59
+ for (const sub of subscribers) {
60
+ try {
61
+ sub(stored);
62
+ }
63
+ catch {
64
+ // Never let a broken subscriber break the tool call path.
65
+ }
66
+ }
67
+ // 3. Write to stderr unless QUIET.
68
+ if (!QUIET) {
69
+ process.stderr.write(JSON.stringify(stored) + '\n');
70
+ }
34
71
  }
35
72
  /**
36
73
  * Wraps a tool handler with timing + structured logging. The returned function
@@ -10,6 +10,8 @@ import { PATTERN as ProductItemCard } from '../src/manifest/patterns/product-ite
10
10
  import { PATTERN as NotificationCard } from '../src/manifest/patterns/notification-card.pattern.js';
11
11
  import { PATTERN as ConfirmationDialog } from '../src/manifest/patterns/confirmation-dialog.pattern.js';
12
12
  import { PATTERN as BulkActionBar } from '../src/manifest/patterns/bulk-action-bar.pattern.js';
13
+ import { PATTERN as ActivityFeed } from '../src/manifest/patterns/activity-feed.pattern.js';
14
+ import { PATTERN as MetricsDashboard } from '../src/manifest/patterns/metrics-dashboard.pattern.js';
13
15
  export const ALL_PATTERNS = [
14
16
  ProfileCard,
15
17
  SettingsPanel,
@@ -23,4 +25,6 @@ export const ALL_PATTERNS = [
23
25
  NotificationCard,
24
26
  ConfirmationDialog,
25
27
  BulkActionBar,
28
+ ActivityFeed,
29
+ MetricsDashboard,
26
30
  ];
@@ -0,0 +1,35 @@
1
+ export const COMPONENT_MANIFEST = {
2
+ id: 'area-chart',
3
+ name: 'AreaChart',
4
+ tier: 'atom',
5
+ domain: 'neutral',
6
+ specVersion: '0.1',
7
+ description: 'A filled line chart for time-series data with optional multi-series support.',
8
+ designIntent: 'Use AreaChart for continuous data over time — revenue trends, user growth, etc. ' +
9
+ 'The translucent fill emphasises volume while the line shows direction. ' +
10
+ 'Multi-series mode auto-assigns colors from the lucent palette. ' +
11
+ 'Pass formatValue to customise y-axis labels (currency, percentages, etc.).',
12
+ props: [
13
+ { name: 'data', type: 'custom', required: false, description: 'Single-series data: { label, value }[].' },
14
+ { name: 'series', type: 'custom', required: false, description: 'Multi-series data: { id, name, data, color? }[].' },
15
+ { name: 'width', type: 'union', required: false, default: "'100%'", description: 'Pixel width or CSS string.' },
16
+ { name: 'height', type: 'number', required: false, default: '200', description: 'Pixel height.' },
17
+ { name: 'color', type: 'string', required: false, default: 'accent', description: 'Line/fill color for single-series.' },
18
+ { name: 'gridLines', type: 'boolean', required: false, default: 'true', description: 'Show horizontal grid lines.' },
19
+ { name: 'showDots', type: 'boolean', required: false, default: 'false', description: 'Show dots at data points.' },
20
+ { name: 'smooth', type: 'boolean', required: false, default: 'true', description: 'Use smooth monotone-x curves.' },
21
+ { name: 'fillOpacity', type: 'number', required: false, default: '0.15', description: 'Area fill opacity (0–1).' },
22
+ { name: 'formatValue', type: 'custom', required: false, description: 'Custom value formatter for y-axis.' },
23
+ { name: 'xLabelCount', type: 'number', required: false, description: 'Max x-axis labels to display.' },
24
+ ],
25
+ usageExamples: [
26
+ { title: 'Basic', code: `<AreaChart data={[{ label: 'Jan', value: 40 }, { label: 'Feb', value: 65 }, { label: 'Mar', value: 50 }]} />` },
27
+ { title: 'Multi-series', code: `<AreaChart series={[{ id: 'a', name: 'Revenue', data: [...] }, { id: 'b', name: 'Costs', data: [...] }]} />` },
28
+ ],
29
+ compositionGraph: [],
30
+ accessibility: {
31
+ role: 'img',
32
+ ariaAttributes: ['aria-label'],
33
+ notes: 'Provide ariaLabel summarising the data trend.',
34
+ },
35
+ };
@@ -0,0 +1,32 @@
1
+ export const COMPONENT_MANIFEST = {
2
+ id: 'bar-chart',
3
+ name: 'BarChart',
4
+ tier: 'atom',
5
+ domain: 'neutral',
6
+ specVersion: '0.1',
7
+ description: 'A vertical bar chart with automatic axis labels and optional grid lines.',
8
+ designIntent: 'Use BarChart for categorical comparisons — revenue by month, counts by category, etc. ' +
9
+ 'Bars auto-scale to fit the data. Set showValues to display exact numbers above each bar. ' +
10
+ 'Per-point color overrides are available for highlighting specific bars.',
11
+ props: [
12
+ { name: 'data', type: 'custom', required: true, description: 'Array of { label, value, color? } objects.' },
13
+ { name: 'width', type: 'union', required: false, default: "'100%'", description: 'Pixel width or CSS string.' },
14
+ { name: 'height', type: 'number', required: false, default: '200', description: 'Pixel height.' },
15
+ { name: 'color', type: 'string', required: false, default: 'accent', description: 'Bar color variant or CSS color.' },
16
+ { name: 'gridLines', type: 'boolean', required: false, default: 'true', description: 'Show horizontal grid lines.' },
17
+ { name: 'showValues', type: 'boolean', required: false, default: 'false', description: 'Show value labels above bars.' },
18
+ { name: 'formatValue', type: 'custom', required: false, description: 'Custom value formatter function.' },
19
+ { name: 'barGap', type: 'number', required: false, default: '0.3', description: 'Gap between bars (0–1 fraction of slot width).' },
20
+ { name: 'barRadius', type: 'number', required: false, default: '2', description: 'Corner radius on bar tops.' },
21
+ ],
22
+ usageExamples: [
23
+ { title: 'Basic', code: `<BarChart data={[{ label: 'Jan', value: 40 }, { label: 'Feb', value: 65 }, { label: 'Mar', value: 50 }]} />` },
24
+ { title: 'With values', code: `<BarChart data={[{ label: 'A', value: 120 }, { label: 'B', value: 80 }]} showValues color="success" />` },
25
+ ],
26
+ compositionGraph: [],
27
+ accessibility: {
28
+ role: 'img',
29
+ ariaAttributes: ['aria-label'],
30
+ notes: 'Provide a descriptive ariaLabel summarising the data for screen readers.',
31
+ },
32
+ };
@@ -0,0 +1,29 @@
1
+ export const COMPONENT_MANIFEST = {
2
+ id: 'donut-chart',
3
+ name: 'DonutChart',
4
+ tier: 'atom',
5
+ domain: 'neutral',
6
+ specVersion: '0.1',
7
+ description: 'A ring chart for displaying proportions with an optional center label.',
8
+ designIntent: 'Use DonutChart for part-to-whole comparisons — category breakdown, budget allocation, etc. ' +
9
+ 'The center label slot is ideal for a total value or summary text. ' +
10
+ 'Segment colors auto-cycle through the lucent palette; override per-segment for semantic meaning.',
11
+ props: [
12
+ { name: 'data', type: 'custom', required: true, description: 'Array of { label, value, color? } segments.' },
13
+ { name: 'size', type: 'number', required: false, default: '160', description: 'Overall pixel size (square).' },
14
+ { name: 'thickness', type: 'number', required: false, default: '0.35', description: 'Ring thickness (0–1 fraction of radius).' },
15
+ { name: 'segmentGap', type: 'number', required: false, default: '2', description: 'Gap between segments in degrees.' },
16
+ { name: 'centerLabel', type: 'custom', required: false, description: 'ReactNode displayed in the center of the ring.' },
17
+ { name: 'startAngle', type: 'number', required: false, default: '0', description: 'Starting angle in degrees (0 = 12 o\'clock).' },
18
+ ],
19
+ usageExamples: [
20
+ { title: 'Basic', code: `<DonutChart data={[{ label: 'A', value: 60 }, { label: 'B', value: 40 }]} />` },
21
+ { title: 'With center label', code: `<DonutChart data={[{ label: 'Used', value: 75 }, { label: 'Free', value: 25 }]} centerLabel={<Text size="lg" weight="bold">75%</Text>} />` },
22
+ ],
23
+ compositionGraph: [],
24
+ accessibility: {
25
+ role: 'img',
26
+ ariaAttributes: ['aria-label'],
27
+ notes: 'Provide ariaLabel describing the proportions for screen readers.',
28
+ },
29
+ };
@@ -0,0 +1,30 @@
1
+ export const COMPONENT_MANIFEST = {
2
+ id: 'sparkline',
3
+ name: 'SparkLine',
4
+ tier: 'atom',
5
+ domain: 'neutral',
6
+ specVersion: '0.1',
7
+ description: 'A compact, inline trend line for displaying data direction at a glance.',
8
+ designIntent: 'Use SparkLine inside KPI cards, table cells, or compact dashboard tiles to show a trend ' +
9
+ 'without axes or labels — surrounding context provides meaning. Defaults to smooth curves ' +
10
+ 'for a polished look; set smooth=false for raw data fidelity.',
11
+ props: [
12
+ { name: 'data', type: 'custom', required: true, description: 'Array of numeric values.' },
13
+ { name: 'width', type: 'union', required: false, default: "'100%'", description: 'Pixel width or CSS string.' },
14
+ { name: 'height', type: 'number', required: false, default: '32', description: 'Pixel height.' },
15
+ { name: 'color', type: 'string', required: false, default: 'accent', description: 'Color variant name or CSS color.' },
16
+ { name: 'filled', type: 'boolean', required: false, default: 'false', description: 'Show translucent area fill below the line.' },
17
+ { name: 'strokeWidth', type: 'number', required: false, default: '1.5', description: 'Line stroke width.' },
18
+ { name: 'smooth', type: 'boolean', required: false, default: 'true', description: 'Use smooth monotone-x curves.' },
19
+ ],
20
+ usageExamples: [
21
+ { title: 'Basic', code: '<SparkLine data={[4, 7, 3, 8, 5, 9, 6]} />' },
22
+ { title: 'Filled', code: '<SparkLine data={[4, 7, 3, 8, 5, 9, 6]} filled color="success" />' },
23
+ ],
24
+ compositionGraph: [],
25
+ accessibility: {
26
+ role: 'img',
27
+ ariaAttributes: ['aria-label'],
28
+ notes: 'Purely decorative — ensure surrounding text provides data context.',
29
+ },
30
+ };
@@ -0,0 +1,82 @@
1
+ export const PATTERN = {
2
+ id: 'activity-feed',
3
+ name: 'Activity Feed',
4
+ description: 'Timestamped event list with user attribution. Each entry shows a timestamp, avatar, action description, and optional content preview card.',
5
+ category: 'dashboard',
6
+ components: ['card', 'avatar', 'text', 'stack', 'row'],
7
+ structure: `
8
+ Card (elevated, padding lg)
9
+ └── Stack gap="4"
10
+ ├── Row justify="between" align="center" ← header
11
+ │ ├── Text (lg, semibold) ← title
12
+ │ └── Text (xs, secondary, uppercase) ← date label
13
+ └── div
14
+ └── ActivityItem[]
15
+ └── div (flex row, gap space-3)
16
+ ├── Text (xs, secondary, w 38px) ← timestamp
17
+ └── div (flex row, gap space-3) ← avatar + content
18
+ ├── Avatar (sm) ← user avatar, 32px
19
+ └── div
20
+ ├── Text (sm) ← name (bold) + action + target (bold)
21
+ └── div? (surface-raised, rounded, padding) ← content preview
22
+ └── Text (sm, secondary) ← preview text
23
+ `.trim(),
24
+ code: `<Card variant="elevated" padding="lg" style={{ width: 600 }}>
25
+ <Stack gap="4">
26
+ <Row justify="between" align="center">
27
+ <Text size="lg" weight="semibold">Activity</Text>
28
+ <Text size="xs" color="secondary" weight="medium"
29
+ style={{ letterSpacing: '0.05em', textTransform: 'uppercase' }}>May 18, 2022</Text>
30
+ </Row>
31
+ <div>
32
+ {/* Event item */}
33
+ <div style={{ display: 'flex', gap: 'var(--lucent-space-3)', paddingBottom: 'var(--lucent-space-5)' }}>
34
+ <div style={{ width: 38, flexShrink: 0, paddingTop: 7 }}>
35
+ <Text size="xs" color="secondary">14:47</Text>
36
+ </div>
37
+ <div style={{ display: 'flex', gap: 'var(--lucent-space-3)', flex: 1, minWidth: 0, alignItems: 'start' }}>
38
+ <Avatar alt="Christoph Hellmuth" size="sm" />
39
+ <div style={{ flex: 1, minWidth: 0, paddingTop: 5 }}>
40
+ <Text size="sm">
41
+ <Text as="span" size="sm" weight="semibold">Christoph Hellmuth</Text>
42
+ {' '}sent message in{' '}
43
+ <Text as="span" size="sm" weight="semibold">#f-insights</Text>
44
+ </Text>
45
+ <div style={{
46
+ marginTop: 'var(--lucent-space-3)',
47
+ padding: 'var(--lucent-space-3) var(--lucent-space-4)',
48
+ background: 'var(--lucent-surface-raised)', borderRadius: 'var(--lucent-radius-md)',
49
+ }}>
50
+ <Text size="sm" color="secondary">Message preview text…</Text>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ </Stack>
57
+ </Card>`,
58
+ variants: [
59
+ {
60
+ title: 'Compact (no header, no previews)',
61
+ code: `<Card variant="outline" padding="md" style={{ width: 480 }}>
62
+ <div>
63
+ <div style={{ display: 'flex', gap: 'var(--lucent-space-2)', paddingBottom: 'var(--lucent-space-3)' }}>
64
+ <Text size="xs" color="secondary" style={{ width: 32, flexShrink: 0 }}>14:47</Text>
65
+ <Avatar alt="Alice" size="xs" style={{ flexShrink: 0 }} />
66
+ <Text size="xs">
67
+ <Text as="span" size="xs" weight="semibold">Alice</Text>
68
+ {' '}pushed to main
69
+ </Text>
70
+ </div>
71
+ </div>
72
+ </Card>`,
73
+ },
74
+ ],
75
+ designNotes: 'Clean multi-column layout gives each information layer its own lane: timestamp on the left ' +
76
+ 'for quick scanning, avatar for identity, then action text. Content previews (message ' +
77
+ 'excerpts, comment text) sit in surface-raised cards below the action line, indented under ' +
78
+ 'the text column so they read as subordinate detail. No timeline connector or source icons — ' +
79
+ 'the vertical rhythm of aligned timestamps provides enough chronological structure without ' +
80
+ 'visual clutter. The timestamp column (38px) is narrow enough to not dominate but wide enough ' +
81
+ 'for HH:MM format.',
82
+ };
@@ -0,0 +1,140 @@
1
+ export const PATTERN = {
2
+ id: 'metrics-dashboard',
3
+ name: 'Metrics Dashboard',
4
+ description: 'KPI cards with progress indicators and tabbed time-range switching (7d / 30d / 90d). Each card shows a metric label, large value, trend chip, and progress bar.',
5
+ category: 'dashboard',
6
+ components: ['card', 'text', 'progress', 'tabs', 'chip', 'stack', 'row'],
7
+ structure: `
8
+ Stack gap="5"
9
+ ├── Row gap="3" align="center" justify="between"
10
+ │ ├── Text (xl, bold) ← section title
11
+ │ └── Tabs (pills, sm) ← time range selector (7d / 30d / 90d)
12
+ └── Row gap="4" wrap
13
+ └── Card[] (elevated, padding lg, flex 1)
14
+ └── Stack gap="3"
15
+ ├── Text (xs, secondary, medium) ← metric label
16
+ ├── Row gap="2" align="baseline"
17
+ │ ├── Text (2xl, bold, display) ← value
18
+ │ └── Chip (success/danger, sm) ← trend delta
19
+ └── Progress (sm) ← completion bar
20
+ `.trim(),
21
+ code: `<Stack gap="5">
22
+ <Row gap="3" align="center" justify="between">
23
+ <Text size="xl" weight="bold">Overview</Text>
24
+ <Tabs
25
+ variant="pills"
26
+ tabs={[
27
+ { value: '7d', label: '7d' },
28
+ { value: '30d', label: '30d' },
29
+ { value: '90d', label: '90d' },
30
+ ]}
31
+ defaultValue="30d"
32
+ style={{ fontSize: 'var(--lucent-font-size-sm)' }}
33
+ />
34
+ </Row>
35
+ <Row gap="4" wrap>
36
+ <Card variant="elevated" padding="lg" style={{ flex: 1, minWidth: 220 }}>
37
+ <Stack gap="3">
38
+ <Text size="xs" color="secondary" weight="medium">Revenue</Text>
39
+ <Row gap="2" align="baseline">
40
+ <Text size="2xl" weight="bold" family="display">$48.2k</Text>
41
+ <Chip variant="success" size="sm" borderless>+12.5%</Chip>
42
+ </Row>
43
+ <Progress value={72} size="sm" />
44
+ </Stack>
45
+ </Card>
46
+ <Card variant="elevated" padding="lg" style={{ flex: 1, minWidth: 220 }}>
47
+ <Stack gap="3">
48
+ <Text size="xs" color="secondary" weight="medium">Active Users</Text>
49
+ <Row gap="2" align="baseline">
50
+ <Text size="2xl" weight="bold" family="display">2,847</Text>
51
+ <Chip variant="success" size="sm" borderless>+8.1%</Chip>
52
+ </Row>
53
+ <Progress value={64} size="sm" />
54
+ </Stack>
55
+ </Card>
56
+ <Card variant="elevated" padding="lg" style={{ flex: 1, minWidth: 220 }}>
57
+ <Stack gap="3">
58
+ <Text size="xs" color="secondary" weight="medium">Churn Rate</Text>
59
+ <Row gap="2" align="baseline">
60
+ <Text size="2xl" weight="bold" family="display">3.2%</Text>
61
+ <Chip variant="danger" size="sm" borderless>+0.8%</Chip>
62
+ </Row>
63
+ <Progress value={32} size="sm" variant="danger" />
64
+ </Stack>
65
+ </Card>
66
+ <Card variant="elevated" padding="lg" style={{ flex: 1, minWidth: 220 }}>
67
+ <Stack gap="3">
68
+ <Text size="xs" color="secondary" weight="medium">Avg. Response</Text>
69
+ <Row gap="2" align="baseline">
70
+ <Text size="2xl" weight="bold" family="display">1.4s</Text>
71
+ <Chip variant="success" size="sm" borderless>-18%</Chip>
72
+ </Row>
73
+ <Progress value={86} size="sm" variant="success" />
74
+ </Stack>
75
+ </Card>
76
+ </Row>
77
+ </Stack>`,
78
+ variants: [
79
+ {
80
+ title: 'Outline cards with threshold-based progress',
81
+ code: `<Stack gap="5">
82
+ <Row gap="3" align="center" justify="between">
83
+ <Text size="xl" weight="bold">System Health</Text>
84
+ <Tabs
85
+ variant="pills"
86
+ tabs={[
87
+ { value: '1h', label: '1h' },
88
+ { value: '24h', label: '24h' },
89
+ { value: '7d', label: '7d' },
90
+ ]}
91
+ defaultValue="24h"
92
+ style={{ fontSize: 'var(--lucent-font-size-sm)' }}
93
+ />
94
+ </Row>
95
+ <Row gap="4" wrap>
96
+ <Card variant="outline" padding="lg" style={{ flex: 1, minWidth: 220 }}>
97
+ <Stack gap="3">
98
+ <Text size="xs" color="secondary" weight="medium">CPU Usage</Text>
99
+ <Row gap="2" align="baseline">
100
+ <Text size="2xl" weight="bold" family="display">67%</Text>
101
+ <Chip variant="warning" size="sm" borderless>+12%</Chip>
102
+ </Row>
103
+ <Progress value={67} size="sm" warnAt={60} dangerAt={85} />
104
+ </Stack>
105
+ </Card>
106
+ <Card variant="outline" padding="lg" style={{ flex: 1, minWidth: 220 }}>
107
+ <Stack gap="3">
108
+ <Text size="xs" color="secondary" weight="medium">Memory</Text>
109
+ <Row gap="2" align="baseline">
110
+ <Text size="2xl" weight="bold" family="display">4.2 GB</Text>
111
+ <Chip variant="success" size="sm" borderless>-3%</Chip>
112
+ </Row>
113
+ <Progress value={52} size="sm" warnAt={70} dangerAt={90} />
114
+ </Stack>
115
+ </Card>
116
+ <Card variant="outline" padding="lg" style={{ flex: 1, minWidth: 220 }}>
117
+ <Stack gap="3">
118
+ <Text size="xs" color="secondary" weight="medium">Disk I/O</Text>
119
+ <Row gap="2" align="baseline">
120
+ <Text size="2xl" weight="bold" family="display">340 MB/s</Text>
121
+ <Chip variant="danger" size="sm" borderless>+45%</Chip>
122
+ </Row>
123
+ <Progress value={91} size="sm" warnAt={60} dangerAt={85} />
124
+ </Stack>
125
+ </Card>
126
+ </Row>
127
+ </Stack>`,
128
+ },
129
+ ],
130
+ designNotes: 'The header row pairs a bold title with pill-style Tabs for time-range switching — ' +
131
+ 'pills read as a toggle group rather than navigation, which matches the "filter" intent. ' +
132
+ 'Each KPI card uses a three-tier Stack: label (xs, secondary) anchors meaning at the top, ' +
133
+ 'the value (2xl, display) commands attention in the middle, and a slim Progress bar at the ' +
134
+ 'bottom gives an at-a-glance ratio without requiring the user to interpret numbers. ' +
135
+ 'The trend Chip sits baseline-aligned next to the value so the delta reads as part of the ' +
136
+ 'same data point. Elevated cards with flex: 1 + minWidth ensure equal sizing that wraps ' +
137
+ 'gracefully at narrow viewports. The system-health variant demonstrates warnAt/dangerAt ' +
138
+ 'thresholds on Progress — the bar auto-colors based on the value, removing the need ' +
139
+ 'for manual variant assignment on each metric.',
140
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lucent-ui",
3
- "version": "0.39.0",
3
+ "version": "0.40.0",
4
4
  "description": "An AI-first React component library with machine-readable manifests.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",