astryx-dracula 0.3.0 → 0.4.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.
- package/USAGE.md +27 -1
- package/astryx-dracula.d.ts +2 -2
- package/astryx-dracula.js +384 -3
- package/astryx-dracula.variants.d.ts +2 -14
- package/icons.mjs +125 -0
- package/package.json +16 -8
- package/shared/revenue-chart.tsx +72 -9
- package/shared/settings-data.ts +43 -1
- package/shared/settings-rows.tsx +81 -30
- package/shared/sparkline.tsx +9 -1
- package/templates/ai-chat-landing.tsx +17 -9
- package/templates/ai-chat.tsx +2 -2
- package/templates/centered-hero.tsx +2 -2
- package/templates/contact-form.tsx +11 -10
- package/templates/dashboard.tsx +7 -3
- package/templates/detail-page.tsx +11 -4
- package/templates/documentation-design.tsx +43 -29
- package/templates/documentation-technical.tsx +11 -9
- package/templates/file-explorer.tsx +82 -70
- package/templates/form-two-column.tsx +1 -1
- package/templates/ide.tsx +6 -6
- package/templates/incident-console.tsx +27 -4
- package/templates/login-card.tsx +8 -5
- package/templates/login-split.tsx +21 -12
- package/templates/login-sso.tsx +57 -41
- package/templates/login.tsx +23 -20
- package/templates/payment-form.tsx +39 -45
- package/templates/product-detail.tsx +4 -6
- package/templates/product-tour.tsx +2 -2
- package/templates/settings-dialog.tsx +84 -87
- package/templates/settings-sidebar.tsx +92 -80
- package/templates/table-grouped.tsx +126 -29
- package/templates/table.tsx +21 -2
- package/templates/tech-report.tsx +1 -1
- package/theme.css +211 -119
- package/tokens.css +15 -7
package/package.json
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
},
|
|
9
9
|
"description": "Pure Dracula brand theme for Astryx React sites. Dark-only tokens, syntax highlighting, and chart colors with prebuilt CSS.",
|
|
10
10
|
"devDependencies": {
|
|
11
|
-
"@astryxdesign/cli": "
|
|
11
|
+
"@astryxdesign/cli": "0.6.3",
|
|
12
|
+
"@astryxdesign/core": "0.6.3",
|
|
12
13
|
"@types/bun": "^1.4.2",
|
|
13
14
|
"@types/node": "^26.5.1",
|
|
14
15
|
"@types/react": "^19.3.0",
|
|
@@ -42,8 +43,8 @@
|
|
|
42
43
|
"./shared/sparkline": "./shared/sparkline.tsx",
|
|
43
44
|
"./shared/sso-icons": "./shared/sso-icons.tsx",
|
|
44
45
|
".": {
|
|
45
|
-
"
|
|
46
|
-
"
|
|
46
|
+
"types": "./astryx-dracula.d.ts",
|
|
47
|
+
"default": "./astryx-dracula.js"
|
|
47
48
|
},
|
|
48
49
|
"./astryx.integration.mjs": "./astryx.integration.mjs",
|
|
49
50
|
"./package.json": "./package.json",
|
|
@@ -60,6 +61,7 @@
|
|
|
60
61
|
"astryx-dracula.js",
|
|
61
62
|
"astryx-dracula.variants.d.ts",
|
|
62
63
|
"fonts/",
|
|
64
|
+
"icons.mjs",
|
|
63
65
|
"icons.tsx",
|
|
64
66
|
"shared/",
|
|
65
67
|
"theme.css",
|
|
@@ -80,7 +82,7 @@
|
|
|
80
82
|
"license": "MIT",
|
|
81
83
|
"name": "astryx-dracula",
|
|
82
84
|
"peerDependencies": {
|
|
83
|
-
"@astryxdesign/core": "^0.3
|
|
85
|
+
"@astryxdesign/core": "^0.6.3",
|
|
84
86
|
"lucide-react": "^1.45.0",
|
|
85
87
|
"react": ">=19.0.0",
|
|
86
88
|
"react-dom": ">=19.0.0"
|
|
@@ -94,9 +96,12 @@
|
|
|
94
96
|
"build": "tsc --noEmit && vite build",
|
|
95
97
|
"dev": "vite",
|
|
96
98
|
"preview": "vite preview",
|
|
97
|
-
"theme:build": "bunx astryx theme build astryx-theme.ts -o theme.css",
|
|
98
|
-
"
|
|
99
|
-
"
|
|
99
|
+
"theme:build": "bun run icons:build && bunx astryx theme build astryx-theme.ts -o theme.css --icons-specifier ./icons.mjs",
|
|
100
|
+
"icons:build": "bun build icons.tsx --outfile icons.mjs --format esm --external react --external lucide-react",
|
|
101
|
+
"theme:check": "bun run icons:check && bunx astryx theme build astryx-theme.ts -o theme.css --icons-specifier ./icons.mjs --check --json | bun -e 'const r = JSON.parse(await Bun.stdin.text()); if (r.type !== \"theme.build.check\") { console.error(\"theme:check: unexpected CLI response \" + JSON.stringify(r).slice(0, 200)); process.exit(1); } const stale = r.data.stale.filter(s => s.path !== \"astryx-dracula.js\"); for (const s of stale) console.error(\"stale: \" + s.path + \" (\" + s.reason + \")\"); if (stale.length) process.exit(1); const entry = await import(\"./astryx-dracula.js\"); if (!entry.astryxDraculaTheme || !entry.draculaIconRegistry) { console.error(\"astryx-dracula.js is missing its theme or icon registry export\"); process.exit(1); } console.log(\"theme outputs up to date; astryx-dracula.js (hand-maintained) loads and exports\");' && bun run entry:check",
|
|
102
|
+
"icons:check": "bun build icons.tsx --outfile .tmp-icons.mjs --format esm --external react --external lucide-react; s=$?; cmp -s .tmp-icons.mjs icons.mjs; c=$?; rm -f .tmp-icons.mjs; [ $s -eq 0 ] && [ $c -eq 0 ] || { echo 'stale: icons.mjs (rerun bun run icons:build)'; exit 1; }",
|
|
103
|
+
"entry:check": "node -e \"const{pathToFileURL}=require('url');const{resolve}=require('path');import(pathToFileURL(resolve('astryx-dracula.js')).href).then(m=>{if(!m.astryxDraculaTheme||!m.draculaIconRegistry){console.error('astryx-dracula.js is missing its theme or icon registry export');process.exit(1)}console.log('astryx-dracula.js loads under plain Node ESM')}).catch(e=>{console.error('astryx-dracula.js is not importable by Node ESM: '+e.message);process.exit(1)})\"",
|
|
104
|
+
"prepublishOnly": "bun run theme:check && bun run icons:check && bun run entry:check && bun run audit && tsc --noEmit && bunx react-doctor@latest --score"
|
|
100
105
|
},
|
|
101
106
|
"sideEffects": [
|
|
102
107
|
"**/*.css"
|
|
@@ -106,5 +111,8 @@
|
|
|
106
111
|
"@astryxdesign/cli"
|
|
107
112
|
],
|
|
108
113
|
"type": "module",
|
|
109
|
-
"version": "0.
|
|
114
|
+
"version": "0.4.0",
|
|
115
|
+
"patchedDependencies": {
|
|
116
|
+
"@astryxdesign/core@0.6.3": "patches/@astryxdesign%2Fcore@0.6.3.patch"
|
|
117
|
+
}
|
|
110
118
|
}
|
package/shared/revenue-chart.tsx
CHANGED
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
import type {CSSProperties} from 'react';
|
|
8
8
|
import {CHART_PANEL_STYLE} from 'astryx-dracula/shared/chart-panel-style';
|
|
9
|
+
import {CHART_HUES} from 'astryx-dracula/shared/chart-hues';
|
|
9
10
|
import {VStack, HStack} from '@astryxdesign/core/Layout';
|
|
10
11
|
import {Text} from '@astryxdesign/core/Text';
|
|
11
12
|
import {Card} from '@astryxdesign/core/Card';
|
|
12
13
|
import {Icon} from '@astryxdesign/core/Icon';
|
|
13
14
|
import {Square} from 'lucide-react';
|
|
14
|
-
import {ChartLabel} from 'astryx-dracula/shared/chart-labels';
|
|
15
|
+
import {ChartLabel, CHART_LABEL_SIZE} from 'astryx-dracula/shared/chart-labels';
|
|
15
16
|
import type {SceneHue} from 'astryx-dracula/shared/scene-hues';
|
|
16
17
|
|
|
17
18
|
// ============= SHARED CHART-PANEL CARD STYLE =============
|
|
@@ -40,7 +41,10 @@ export function ProductSwatch({
|
|
|
40
41
|
style={swatchStyle}
|
|
41
42
|
role="img"
|
|
42
43
|
aria-label={`${label} swatch`}>
|
|
43
|
-
|
|
44
|
+
{/* A surface, not a data mark: the plate behind a product swatch wears the
|
|
45
|
+
card tier. --color-background-card is byte-identical to the old
|
|
46
|
+
--dracula-bg-light (#343746). */}
|
|
47
|
+
<rect width={36} height={36} rx={5} fill="var(--color-background-card)" />
|
|
44
48
|
<rect
|
|
45
49
|
x={1}
|
|
46
50
|
y={1}
|
|
@@ -64,7 +68,10 @@ export function ProductSwatch({
|
|
|
64
68
|
|
|
65
69
|
// ============= REVENUE CHART =============
|
|
66
70
|
|
|
67
|
-
|
|
71
|
+
// Role token, not a --dracula-* primitive. Byte-identical (--color-data-
|
|
72
|
+
// categorical-cyan IS #8BE9FD) and routed through chart-hues.ts so the line
|
|
73
|
+
// colour is the sanctioned vocabulary rather than a literal decided here.
|
|
74
|
+
const REVENUE_LINE = CHART_HUES.cyan;
|
|
68
75
|
const CHART_W = 540;
|
|
69
76
|
const CHART_H = 200;
|
|
70
77
|
const CHART_PAD_LEFT = 44;
|
|
@@ -73,6 +80,23 @@ const CHART_PAD_TOP = 12;
|
|
|
73
80
|
const CHART_BASELINE = 164;
|
|
74
81
|
const CHART_PLOT_W = CHART_W - CHART_PAD_LEFT - CHART_PAD_RIGHT;
|
|
75
82
|
|
|
83
|
+
// X-axis label budget, counted in ticks rather than baked into a stride: five
|
|
84
|
+
// intervals is the most the 484-unit plot carries with a tick-width of air
|
|
85
|
+
// still between neighbours — a sixth drops the tightest pair to ~20px on a
|
|
86
|
+
// 390px viewport, and the final pair needs room on top of that. Six ticks is
|
|
87
|
+
// the ceiling, and the stride comes from the point count.
|
|
88
|
+
const CHART_MAX_X_TICKS = 6;
|
|
89
|
+
// A tick is as wide as its own text — the mono face advances 0.6em a glyph, so
|
|
90
|
+
// `Jan 1` and `Jan 13` are different widths, and the last pair is decided by
|
|
91
|
+
// that difference. Measured, not assumed.
|
|
92
|
+
const CHART_TICK_ADVANCE = 0.6 * CHART_LABEL_SIZE;
|
|
93
|
+
// The final tick is end-anchored here instead of centred on its own point,
|
|
94
|
+
// which is what buys it room inside the right pad. The price is the
|
|
95
|
+
// `CHART_TICK_PIN` it hangs back plus half a tick box of approach from the
|
|
96
|
+
// left, and that is the air the grid below has to leave it.
|
|
97
|
+
const CHART_LAST_TICK_X = CHART_W - CHART_PAD_RIGHT - 2;
|
|
98
|
+
const CHART_TICK_PIN = CHART_PAD_LEFT + CHART_PLOT_W - CHART_LAST_TICK_X;
|
|
99
|
+
|
|
76
100
|
interface RevenuePoint {
|
|
77
101
|
date: string;
|
|
78
102
|
revenue: number;
|
|
@@ -83,6 +107,48 @@ function formatRevenueTick(tick: number): string {
|
|
|
83
107
|
return tick >= 1000 ? `$${tick / 1000}k` : `$${tick}`;
|
|
84
108
|
}
|
|
85
109
|
|
|
110
|
+
// Which points get a date tick. A fixed stride cannot answer this: `i % 3`
|
|
111
|
+
// plus a forced final tick puts that final tick 0–1 slots from its neighbour
|
|
112
|
+
// for two point counts in three, and one slot is `Jan 13` and `Jan 15` set
|
|
113
|
+
// solid. So the stride comes from the point count, and the final point only
|
|
114
|
+
// joins the axis once the pin has left it its air; when it has not, the
|
|
115
|
+
// crowded grid tick gives way instead, so the axis ends on a gap wider than
|
|
116
|
+
// every other one rather than on the narrowest one.
|
|
117
|
+
function xTickIndices(dates: string[]): number[] {
|
|
118
|
+
const last = dates.length - 1;
|
|
119
|
+
if (last < 1) return [last];
|
|
120
|
+
const step = CHART_PLOT_W / last;
|
|
121
|
+
const width = (i: number) => dates[i].length * CHART_TICK_ADVANCE;
|
|
122
|
+
// Clear space between two ticks: `slots` apart, less the half-box each side
|
|
123
|
+
// shows. The final tick is the exception — it shows its whole box to the left
|
|
124
|
+
// of its anchor, so its pair is the one that has to clear the most.
|
|
125
|
+
const gap = (a: number, b: number) =>
|
|
126
|
+
(b - a) * step -
|
|
127
|
+
(width(a) + width(b)) / 2 -
|
|
128
|
+
(b === last ? CHART_TICK_PIN + width(b) / 2 : 0);
|
|
129
|
+
const stride = Math.max(1, Math.ceil(last / (CHART_MAX_X_TICKS - 1)));
|
|
130
|
+
// The grid, minus whatever the pinned final tick has crowded out. Every
|
|
131
|
+
// step of `end` down leaves the grid spacing even and gives the last pair
|
|
132
|
+
// one stride more room, so the first grid that clears is the one to keep.
|
|
133
|
+
let ticks = [0, last];
|
|
134
|
+
for (let end = Math.floor(last / stride) * stride; end >= 0; end -= stride) {
|
|
135
|
+
const grid: number[] = [];
|
|
136
|
+
for (let i = 0; i <= end; i += stride) grid.push(i);
|
|
137
|
+
if (grid[grid.length - 1] !== last) grid.push(last);
|
|
138
|
+
// Every pair, in order, so the last one can be held to the tightest of
|
|
139
|
+
// the rest instead of to the average.
|
|
140
|
+
const gaps = grid
|
|
141
|
+
.slice(0, -1)
|
|
142
|
+
.map((t, k) => gap(t, grid[k + 1]));
|
|
143
|
+
const interior = gaps.slice(0, -1);
|
|
144
|
+
if (interior.length === 0 || gaps[gaps.length - 1] >= Math.min(...interior)) {
|
|
145
|
+
ticks = grid;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return ticks;
|
|
150
|
+
}
|
|
151
|
+
|
|
86
152
|
export function RevenueChart({
|
|
87
153
|
data,
|
|
88
154
|
chartMax,
|
|
@@ -102,6 +168,7 @@ export function RevenueChart({
|
|
|
102
168
|
x: CHART_PAD_LEFT + (i / (data.length - 1)) * CHART_PLOT_W,
|
|
103
169
|
y: CHART_BASELINE - (d.revenue / chartMax) * plotH,
|
|
104
170
|
}));
|
|
171
|
+
const dateTicks = new Set(xTickIndices(data.map(d => d.date)));
|
|
105
172
|
const linePath = points
|
|
106
173
|
.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x.toFixed(1)},${p.y.toFixed(1)}`)
|
|
107
174
|
.join(' ');
|
|
@@ -144,14 +211,10 @@ export function RevenueChart({
|
|
|
144
211
|
/>
|
|
145
212
|
{points.map(
|
|
146
213
|
(p, i) =>
|
|
147
|
-
(i
|
|
214
|
+
dateTicks.has(i) && (
|
|
148
215
|
<ChartLabel
|
|
149
216
|
key={p.date}
|
|
150
|
-
x={
|
|
151
|
-
i === points.length - 1
|
|
152
|
-
? CHART_W - CHART_PAD_RIGHT - 2
|
|
153
|
-
: p.x
|
|
154
|
-
}
|
|
217
|
+
x={i === points.length - 1 ? CHART_LAST_TICK_X : p.x}
|
|
155
218
|
y={CHART_H - 8}
|
|
156
219
|
textAnchor={i === points.length - 1 ? 'end' : 'middle'}>
|
|
157
220
|
{p.date}
|
package/shared/settings-data.ts
CHANGED
|
@@ -42,18 +42,60 @@ export const NAV_ITEMS: SettingsNavItem[] = [
|
|
|
42
42
|
{label: 'Travel for work', icon: Briefcase},
|
|
43
43
|
];
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* How a row's trailing Button reads. Deliberately narrower than core's
|
|
47
|
+
* `ButtonVariant`: a trailing row control is not a place to choose freely, and
|
|
48
|
+
* two of core's four options can never be right here — `primary` would
|
|
49
|
+
* outrank the panel's own primary, `ghost` would leave an action with no
|
|
50
|
+
* affordance at all. Only the two that can be are offered.
|
|
51
|
+
*/
|
|
52
|
+
export type RowActionVariant = 'secondary' | 'destructive';
|
|
45
53
|
export interface InfoRow {
|
|
46
54
|
label: string;
|
|
47
55
|
value: string;
|
|
48
56
|
action: string;
|
|
57
|
+
/**
|
|
58
|
+
* What `action` actually is. 'action' (the default) changes state in place
|
|
59
|
+
* and renders a Button; 'destination' navigates and renders a Link. Stated
|
|
60
|
+
* in the data rather than inferred from `href` or read off the label, so
|
|
61
|
+
* renaming "View" cannot silently turn a destination back into an action.
|
|
62
|
+
*/
|
|
63
|
+
actionKind?: 'action' | 'destination';
|
|
64
|
+
/** Where a 'destination' action goes. Read only when actionKind is 'destination'. */
|
|
65
|
+
href?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Visual weight of the trailing Button. `secondary` is the default and the
|
|
68
|
+
* right answer for almost every row. `destructive` is for a row whose action
|
|
69
|
+
* ends or destroys something — disconnecting an account, deleting data — so
|
|
70
|
+
* that control agrees with the templates' inline Deactivate/Delete buttons
|
|
71
|
+
* instead of rendering identically to "Create" sitting above it. Stated
|
|
72
|
+
* here for the same reason `actionKind` is: the renderer cannot recover it
|
|
73
|
+
* from the label without reading the English verb off the row, which is how
|
|
74
|
+
* "View" became a button in the first place. Ignored for a `'destination'`
|
|
75
|
+
* row, which renders a Link and takes no variant.
|
|
76
|
+
*/
|
|
77
|
+
variant?: RowActionVariant;
|
|
78
|
+
/**
|
|
79
|
+
* What the Button does, when something wants it to. Optional on purpose: a
|
|
80
|
+
* row with no `onAction` renders an inert control, which is the honest
|
|
81
|
+
* default for a showcase with no backend — better than a toggle that flips
|
|
82
|
+
* "Connected" to "Disconnected" and pretends a call happened. A consumer
|
|
83
|
+
* that has a real handler passes one and the row goes live. Nothing here
|
|
84
|
+
* simulates a result or invents state to make the button look busy.
|
|
85
|
+
*/
|
|
86
|
+
onAction?: () => void;
|
|
49
87
|
}
|
|
50
88
|
|
|
51
89
|
export const LOGIN_ROWS: InfoRow[] = [
|
|
52
90
|
{label: 'Password', value: 'Not created', action: 'Create'},
|
|
53
91
|
];
|
|
54
92
|
|
|
93
|
+
// Disconnecting ends an account link the user set up on purpose, so it is the
|
|
94
|
+
// one destructive row in this file — marked here, from the verb, not by
|
|
95
|
+
// position. LOGIN_ROWS' "Create" is the opposite (it adds a password) and
|
|
96
|
+
// keeps the default `secondary`.
|
|
55
97
|
export const SOCIAL_ROWS: InfoRow[] = [
|
|
56
|
-
{label: 'Google', value: 'Connected', action: 'Disconnect'},
|
|
98
|
+
{label: 'Google', value: 'Connected', action: 'Disconnect', variant: 'destructive'},
|
|
57
99
|
];
|
|
58
100
|
|
|
59
101
|
export interface DeviceRow {
|
package/shared/settings-rows.tsx
CHANGED
|
@@ -26,32 +26,76 @@ import {
|
|
|
26
26
|
SquarePen,
|
|
27
27
|
Share2,
|
|
28
28
|
} from 'lucide-react';
|
|
29
|
-
import type {CSSProperties,
|
|
29
|
+
import type {CSSProperties, ReactNode} from 'react';
|
|
30
30
|
import {VStack, HStack} from '@astryxdesign/core/Layout';
|
|
31
31
|
import {Text} from '@astryxdesign/core/Text';
|
|
32
|
-
import {Link} from '@astryxdesign/core/Link';
|
|
33
32
|
import {Button} from '@astryxdesign/core/Button';
|
|
33
|
+
import {Link} from '@astryxdesign/core/Link';
|
|
34
34
|
import {Divider} from '@astryxdesign/core/Divider';
|
|
35
35
|
|
|
36
36
|
// ─── Row components ──────────────────────────────────────────────────────────
|
|
37
37
|
// The row renderers themselves, moved here from both templates. The two copies
|
|
38
|
-
// were byte-identical apart from
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
// exactly; only these
|
|
38
|
+
// were byte-identical apart from things that genuinely differ per template, so
|
|
39
|
+
// those are props rather than forks: `style` (the sidebar pads its rows, the
|
|
40
|
+
// dialog does not) and `hasDivider` (each template draws its own group
|
|
41
|
+
// boundaries). Verified by hash before the move — the row BODIES matched
|
|
42
|
+
// exactly; only these call-site differences remained.
|
|
43
|
+
//
|
|
44
|
+
// FOUR RULES these rows are built around, from USAGE.md "Rules":
|
|
45
|
+
// - A link is a destination, a button is an action. The trailing control is a
|
|
46
|
+
// `Button` when the row acts in place and a `Link` when it navigates, and
|
|
47
|
+
// the row data says which: `actionKind` is `'action'` (the default) for
|
|
48
|
+
// "Create"/"Disconnect", `'destination'` for "View" on a list of past
|
|
49
|
+
// records, with `href` carrying the destination. It is stated in the data
|
|
50
|
+
// rather than guessed from the label, because reading the English verb off
|
|
51
|
+
// the row is what made "View" a button. The Link takes the kit's
|
|
52
|
+
// hover-only underline — it jumps out of the panel, which is the
|
|
53
|
+
// navigation case, not prose — and never a hand-rolled `textDecoration`.
|
|
54
|
+
// - The Button is configured from the row data, never from a constant here.
|
|
55
|
+
// `variant` defaults to `secondary` and a row opts into `destructive` when
|
|
56
|
+
// its action ends or destroys something, so a trailing "Disconnect" reads
|
|
57
|
+
// like the panels' own inline Deactivate/Delete controls instead of like
|
|
58
|
+
// the "Create" sitting above it. It is never `primary`: a row action must
|
|
59
|
+
// not outrank the panel's own primary, and a filled accent button in a
|
|
60
|
+
// trailing column is what made these panels read as a form.
|
|
61
|
+
// `onAction` becomes the Button's `onClick` and is optional — with no
|
|
62
|
+
// handler the Button renders inert, which is the honest default for a
|
|
63
|
+
// showcase with no backend. A consumer with a real handler passes one and
|
|
64
|
+
// the row goes live; nothing here fabricates state to make it look live.
|
|
65
|
+
// - A divider is a section boundary, not a row background. Rows therefore
|
|
66
|
+
// render one ONLY when the caller marks `hasDivider` at a real group
|
|
67
|
+
// boundary; without it the row leaves the rhythm to the caller's own
|
|
68
|
+
// `Stack gap` / row padding rather than adding a rule of its own.
|
|
69
|
+
// - Spacing comes from the scale. The label/value pair is spaced with
|
|
70
|
+
// `Stack gap`, never a margin, and `gap={1}` matches the rows each template
|
|
71
|
+
// builds itself (device history, Deactivate, the info tiles) so no row on
|
|
72
|
+
// the page reads tighter than its neighbours.
|
|
73
|
+
//
|
|
74
|
+
// `href` is a real field on `InfoRow`: where a `'destination'` action goes,
|
|
75
|
+
// read only when `actionKind` says the row navigates. The inert `href` prop on
|
|
76
|
+
// `ExpandableRow` stays for call-site compatibility — it anchored Links this
|
|
77
|
+
// file no longer renders, nothing reads it now, and removing it would break
|
|
78
|
+
// any caller that still passes it.
|
|
43
79
|
|
|
44
80
|
export function InfoRowItem({
|
|
45
81
|
label,
|
|
46
82
|
value,
|
|
47
83
|
action,
|
|
84
|
+
actionKind,
|
|
48
85
|
href,
|
|
86
|
+
variant = 'secondary',
|
|
87
|
+
onAction,
|
|
49
88
|
style,
|
|
50
|
-
|
|
89
|
+
hasDivider = true,
|
|
90
|
+
}: InfoRow & {
|
|
91
|
+
style?: CSSProperties;
|
|
92
|
+
/** Draw a Divider after the row. True only at a group boundary. */
|
|
93
|
+
hasDivider?: boolean;
|
|
94
|
+
}) {
|
|
51
95
|
return (
|
|
52
96
|
<>
|
|
53
97
|
<HStack hAlign="between" vAlign="start" style={style}>
|
|
54
|
-
<VStack gap={
|
|
98
|
+
<VStack gap={1}>
|
|
55
99
|
<Text type="body" weight="semibold" display="block">
|
|
56
100
|
{label}
|
|
57
101
|
</Text>
|
|
@@ -59,13 +103,22 @@ export function InfoRowItem({
|
|
|
59
103
|
{value}
|
|
60
104
|
</Text>
|
|
61
105
|
</VStack>
|
|
62
|
-
{action &&
|
|
63
|
-
|
|
64
|
-
{
|
|
65
|
-
|
|
66
|
-
|
|
106
|
+
{action &&
|
|
107
|
+
(actionKind === 'destination' && href ? (
|
|
108
|
+
<Link href={href} style={actionNoWrap}>
|
|
109
|
+
{action}
|
|
110
|
+
</Link>
|
|
111
|
+
) : (
|
|
112
|
+
<Button
|
|
113
|
+
label={action}
|
|
114
|
+
variant={variant}
|
|
115
|
+
size="sm"
|
|
116
|
+
style={actionNoWrap}
|
|
117
|
+
onClick={onAction}
|
|
118
|
+
/>
|
|
119
|
+
))}
|
|
67
120
|
</HStack>
|
|
68
|
-
<Divider />
|
|
121
|
+
{hasDivider && <Divider />}
|
|
69
122
|
</>
|
|
70
123
|
);
|
|
71
124
|
}
|
|
@@ -101,18 +154,16 @@ function ExpandableRowViewing({
|
|
|
101
154
|
label,
|
|
102
155
|
value,
|
|
103
156
|
onEdit,
|
|
104
|
-
href,
|
|
105
157
|
style,
|
|
106
158
|
}: {
|
|
107
159
|
label: string;
|
|
108
160
|
value: string;
|
|
109
161
|
onEdit: () => void;
|
|
110
|
-
href: string;
|
|
111
162
|
style?: CSSProperties;
|
|
112
163
|
}) {
|
|
113
164
|
return (
|
|
114
165
|
<HStack hAlign="between" vAlign="start" style={style}>
|
|
115
|
-
<VStack gap={
|
|
166
|
+
<VStack gap={1}>
|
|
116
167
|
<Text type="body" weight="semibold" display="block">
|
|
117
168
|
{label}
|
|
118
169
|
</Text>
|
|
@@ -120,15 +171,13 @@ function ExpandableRowViewing({
|
|
|
120
171
|
{value}
|
|
121
172
|
</Text>
|
|
122
173
|
</VStack>
|
|
123
|
-
<
|
|
124
|
-
|
|
174
|
+
<Button
|
|
175
|
+
label="Edit"
|
|
176
|
+
variant="secondary"
|
|
177
|
+
size="sm"
|
|
125
178
|
style={actionNoWrap}
|
|
126
|
-
onClick={
|
|
127
|
-
|
|
128
|
-
onEdit();
|
|
129
|
-
}}>
|
|
130
|
-
Edit
|
|
131
|
-
</Link>
|
|
179
|
+
onClick={onEdit}
|
|
180
|
+
/>
|
|
132
181
|
</HStack>
|
|
133
182
|
);
|
|
134
183
|
}
|
|
@@ -141,8 +190,8 @@ export function ExpandableRow({
|
|
|
141
190
|
onEdit,
|
|
142
191
|
onCancel,
|
|
143
192
|
onSave,
|
|
144
|
-
href,
|
|
145
193
|
style,
|
|
194
|
+
hasDivider = true,
|
|
146
195
|
}: {
|
|
147
196
|
label: string;
|
|
148
197
|
value: string;
|
|
@@ -151,8 +200,11 @@ export function ExpandableRow({
|
|
|
151
200
|
onEdit: () => void;
|
|
152
201
|
onCancel: () => void;
|
|
153
202
|
onSave: () => void;
|
|
154
|
-
|
|
203
|
+
/** Accepted for call-site compatibility; unused — see the note above. */
|
|
204
|
+
href?: string;
|
|
155
205
|
style?: CSSProperties;
|
|
206
|
+
/** Draw a Divider after the row. True only at a group boundary. */
|
|
207
|
+
hasDivider?: boolean;
|
|
156
208
|
}) {
|
|
157
209
|
return (
|
|
158
210
|
<>
|
|
@@ -169,11 +221,10 @@ export function ExpandableRow({
|
|
|
169
221
|
label={label}
|
|
170
222
|
value={value}
|
|
171
223
|
onEdit={onEdit}
|
|
172
|
-
href={href}
|
|
173
224
|
style={style}
|
|
174
225
|
/>
|
|
175
226
|
)}
|
|
176
|
-
<Divider />
|
|
227
|
+
{hasDivider && <Divider />}
|
|
177
228
|
</>
|
|
178
229
|
);
|
|
179
230
|
}
|
package/shared/sparkline.tsx
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// geometry and the portfolio range-normalized step geometry behind one mode
|
|
3
3
|
// switch. Purple stays out of the chart so it keeps meaning "interactive".
|
|
4
4
|
|
|
5
|
+
import {CHART_HUES} from 'astryx-dracula/shared/chart-hues';
|
|
6
|
+
|
|
5
7
|
export interface SparkPoint {
|
|
6
8
|
id: string;
|
|
7
9
|
value: number;
|
|
@@ -28,7 +30,13 @@ export function Sparkline({
|
|
|
28
30
|
mode = 'max',
|
|
29
31
|
isCompact = false,
|
|
30
32
|
}: SparklineProps) {
|
|
31
|
-
|
|
33
|
+
// Role tokens, not --dracula-* primitives. Byte-identical values
|
|
34
|
+
// (--color-data-categorical-green IS #50FA7B), so this is a vocabulary
|
|
35
|
+
// change with no visual delta -- it just means the sparkline agrees with
|
|
36
|
+
// chart-hues.ts instead of being the one chart module that decided alone.
|
|
37
|
+
// Reported by a consumer whose test asserted their sparkline fill equalled
|
|
38
|
+
// their CHART_HUES.green and stopped agreeing across a version bump.
|
|
39
|
+
const fill = positive ? CHART_HUES.green : CHART_HUES.red;
|
|
32
40
|
if (mode === 'range') {
|
|
33
41
|
const values = data.map(point => point.value);
|
|
34
42
|
const max = Math.max(...values);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
// XLE (canonical structure, validated with `bunx astryx layout check`):
|
|
3
|
-
// L > LC > V[g=8 a=center] > (V[g=1] > (H[g=2 a=center] > Ic + Tx"Hi, Vlad"[t=body]) + Hd"Where should we start?"[level=1 type=display-2]) + ChC"Ask anything" + (V[g=6] > (TgG"Category" > Tg"Writing"*4) + Hd"Suggested prompts"[level=2] + (G[c={min:280} g=3] > (CC[p=4] > V[g=0.5] > Hd"Draft"[level=3] + Tx"Compose"[t=body])*4))
|
|
3
|
+
// L > LC > V[g=8 a=center] > (V[g=1] > (H[g=2 a=center] > Ic + Tx"Hi, Vlad"[t=body]) + Hd"Where should we start?"[level=1 type=display-2]) + ChC"Ask anything" + (V[g=6] > (TgG"Category" > H[g=1 wrap] > Tg"Writing"*4) + Hd"Suggested prompts"[level=2] + (G[c={min:280} g=3] > (CC[p=4] > V[g=0.5] > Hd"Draft"[level=3] + Tx"Compose"[t=body])*4))
|
|
4
4
|
|
|
5
5
|
import {useRef, useState, type CSSProperties} from 'react';
|
|
6
6
|
|
|
@@ -467,14 +467,22 @@ export default function AiChatLanding() {
|
|
|
467
467
|
value={category}
|
|
468
468
|
onChange={setCategory}
|
|
469
469
|
size="lg">
|
|
470
|
-
{
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
470
|
+
{/* ToggleButtonGroup is a single nowrap flex row and the four
|
|
471
|
+
chips measure 488px together. In a 390px pane the fourth
|
|
472
|
+
chip was cut in half by the viewport edge, which reads as a
|
|
473
|
+
rendering fault rather than as overflow. The wrapping
|
|
474
|
+
HStack gives the row a second line instead; the group keeps
|
|
475
|
+
its own selection semantics either way. */}
|
|
476
|
+
<HStack gap={1} wrap="wrap">
|
|
477
|
+
{CATEGORIES.map(cat => (
|
|
478
|
+
<ToggleButton
|
|
479
|
+
key={cat.key}
|
|
480
|
+
value={cat.key}
|
|
481
|
+
label={cat.label}
|
|
482
|
+
icon={<Icon icon={cat.icon} size="sm" />}
|
|
483
|
+
/>
|
|
484
|
+
))}
|
|
485
|
+
</HStack>
|
|
478
486
|
</ToggleButtonGroup>
|
|
479
487
|
|
|
480
488
|
{suggestions && (
|
package/templates/ai-chat.tsx
CHANGED
|
@@ -301,8 +301,8 @@ export default function AiChat() {
|
|
|
301
301
|
const rootRef = useRef<HTMLElement>(null);
|
|
302
302
|
const artifactResize = useResizable({
|
|
303
303
|
defaultSize: 640,
|
|
304
|
-
|
|
305
|
-
|
|
304
|
+
minSize: 480,
|
|
305
|
+
maxSize: 960,
|
|
306
306
|
autoSaveId: 'ai-chat-artifact-panel',
|
|
307
307
|
});
|
|
308
308
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
// XLE (canonical structure, validated with `bunx astryx layout check`):
|
|
3
|
-
// L > LC[p=6] > V[g=10] > ((V[g=6 a=center] > (V[g=3 a=center] > Hd"Small joys for creatures of the night"[level=1 t=display-2] + Tx"Sometimes all it takes is one small ritual to turn the whole night around."[t=body] + Tx"astryx-dracula v0.
|
|
3
|
+
// L > LC[p=6] > V[g=10] > ((V[g=6 a=center] > (V[g=3 a=center] > Hd"Small joys for creatures of the night"[level=1 t=display-2] + Tx"Sometimes all it takes is one small ritual to turn the whole night around."[t=body] + Tx"astryx-dracula v0.4.0"[t=supporting]) + (H[j=center g=3] > B.primary"Embrace the night" + B.secondary"Read the lore")) + (S.transparent[p=0] > AR[ratio=16/9]))
|
|
4
4
|
|
|
5
5
|
import type {CSSProperties} from 'react';
|
|
6
6
|
import {VStack, HStack, Layout, LayoutContent} from '@astryxdesign/core/Layout';
|
|
@@ -52,7 +52,7 @@ export default function CenteredHero() {
|
|
|
52
52
|
color="secondary"
|
|
53
53
|
justify="center"
|
|
54
54
|
hasTabularNumbers>
|
|
55
|
-
astryx-dracula v0.
|
|
55
|
+
astryx-dracula v0.4.0
|
|
56
56
|
</Text>
|
|
57
57
|
</VStack>
|
|
58
58
|
<HStack gap={3} wrap="wrap" justify="center">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
// XLE (canonical structure, validated with `bunx astryx layout check`):
|
|
3
|
-
// Ctr > V[a=center] > S.transparent[p=6] > V[g=6] > (V[g=2 a=center] > Hd"Let's brew after dark"[level=1] + Tx"Tell us a bit"[t=body]) + (V[g=4] > Hd"Why work with us"[level=2] + (G[c={min:200} g=4] > (C > V[g=3] > Ic + (V[g=1] > Hd"Title"[level=3] + Tx"Desc"[t=body]))*3)) + (V[g=4] > Hd"Your details"[level=2] + (G[c={min:260} g=4] > TI"Full name" + TI"Email") + (G[c={min:260} g=4] > TI"Company" + TI"Phone")) + D + (V[g=4] > Hd"Your project"[level=2] + (Fd"Goals"[req] > (H[g=2] > Tk"Goal"*10)) + SE"Timeline" + SE"Budget" + (RL"Source" > RLI*5) + TA"Notes" + CB"Decision maker") + (V[g=3] > B.primary"Send it into the night" + (
|
|
3
|
+
// Ctr > V[a=center] > S.transparent[p=6] > V[g=6] > (V[g=2 a=center] > Hd"Let's brew after dark"[level=1] + Tx"Tell us a bit"[t=body]) + (V[g=4] > Hd"Why work with us"[level=2] + (G[c={min:200} g=4] > (C > V[g=3] > Ic + (V[g=1] > Hd"Title"[level=3] + Tx"Desc"[t=body]))*3)) + (V[g=4] > Hd"Your details"[level=2] + (G[c={min:260} g=4] > TI"Full name" + TI"Email") + (G[c={min:260} g=4] > TI"Company" + TI"Phone")) + D + (V[g=4] > Hd"Your project"[level=2] + (Fd"Goals"[req] > (H[g=2] > Tk"Goal"*10)) + SE"Timeline" + SE"Budget" + (RL"Source" > RLI*5) + TA"Notes" + CB"Decision maker") + (V[g=3] > B.primary"Send it into the night" + (Tx[t=supporting] > Lk"Privacy Policy"))
|
|
4
4
|
|
|
5
5
|
import {useState} from 'react';
|
|
6
6
|
import {VStack, HStack} from '@astryxdesign/core/Layout';
|
|
@@ -296,15 +296,16 @@ export default function ContactForm() {
|
|
|
296
296
|
variant="primary"
|
|
297
297
|
onClick={() => setSubmitted(true)}
|
|
298
298
|
/>
|
|
299
|
-
<
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
</
|
|
307
|
-
|
|
299
|
+
<Text type="supporting" color="secondary">
|
|
300
|
+
By submitting you agree to our{' '}
|
|
301
|
+
<Link
|
|
302
|
+
href="#/templates/contact-form"
|
|
303
|
+
type="supporting"
|
|
304
|
+
hasUnderline>
|
|
305
|
+
Privacy Policy
|
|
306
|
+
</Link>
|
|
307
|
+
.
|
|
308
|
+
</Text>
|
|
308
309
|
</VStack>
|
|
309
310
|
</VStack>
|
|
310
311
|
</Section>
|
package/templates/dashboard.tsx
CHANGED
|
@@ -19,6 +19,10 @@ import {Divider} from '@astryxdesign/core/Divider';
|
|
|
19
19
|
import {Link} from '@astryxdesign/core/Link';
|
|
20
20
|
import {Icon} from '@astryxdesign/core/Icon';
|
|
21
21
|
|
|
22
|
+
// Same-route hash: demo links stay focusable anchors without escaping the
|
|
23
|
+
// template through the hash router (bare "#" would drop back to the home page).
|
|
24
|
+
const SELF_HASH = '#/templates/dashboard';
|
|
25
|
+
|
|
22
26
|
// ============= ICONS =============
|
|
23
27
|
|
|
24
28
|
import {RefreshCw, Square} from 'lucide-react';
|
|
@@ -632,7 +636,7 @@ function StackedBarCard({
|
|
|
632
636
|
{/* Legend */}
|
|
633
637
|
<HStack gap={4} wrap="wrap">
|
|
634
638
|
{data.map(d => (
|
|
635
|
-
<VStack key={d.label} gap={
|
|
639
|
+
<VStack key={d.label} gap={1}>
|
|
636
640
|
<HStack gap={2} vAlign="center">
|
|
637
641
|
<Icon icon={Square} size="xsm" style={{color: d.color}} />
|
|
638
642
|
<Text type="supporting" color="secondary">{d.label}</Text>
|
|
@@ -756,14 +760,14 @@ export default function DashboardTemplate() {
|
|
|
756
760
|
<TableCard
|
|
757
761
|
title="Top pages"
|
|
758
762
|
linkLabel="All pages"
|
|
759
|
-
linkHref=
|
|
763
|
+
linkHref={SELF_HASH}
|
|
760
764
|
data={topPagesData}
|
|
761
765
|
columns={topPagesColumns}
|
|
762
766
|
/>
|
|
763
767
|
<TableCard
|
|
764
768
|
title="Top events"
|
|
765
769
|
linkLabel="All events"
|
|
766
|
-
linkHref=
|
|
770
|
+
linkHref={SELF_HASH}
|
|
767
771
|
data={topEventsData}
|
|
768
772
|
columns={topEventsColumns}
|
|
769
773
|
/>
|