@titan-design/react-ui 0.9.0 → 0.9.2
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/dist/index.d.mts +13 -3
- package/dist/index.d.ts +13 -3
- package/dist/index.js +20 -21
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +20 -21
- package/dist/index.mjs.map +1 -1
- package/dist/pages.js +2 -1
- package/dist/pages.js.map +1 -1
- package/dist/pages.mjs +2 -1
- package/dist/pages.mjs.map +1 -1
- package/package.json +2 -1
- package/src/components/custom/Workout/ExerciseHeading.tsx +5 -3
- package/src/components/custom/Workout/SessionHeader.tsx +6 -5
- package/src/components/custom/Workout/SessionRail.tsx +18 -7
- package/src/components/custom/Workout/SetsRepsLoad.tsx +2 -1
- package/src/components/shell/DashboardShell.tsx +7 -9
- package/src/components/shell/NavItem.tsx +6 -1
- package/src/test/no-device-internals.test.ts +51 -0
- package/src/lab/north-star/LivePage.tsx +0 -101
- package/src/lab/north-star/LiveView.tsx +0 -409
- package/src/lab/north-star/LiveWallDashboard.stories.tsx +0 -129
- package/src/lab/north-star/RestView.tsx +0 -140
- package/src/lab/north-star/fixtures.ts +0 -224
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@titan-design/react-ui",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "Cross-platform design system built on Gluestack UI",
|
|
5
5
|
"author": "Henry Jewkes",
|
|
6
6
|
"license": "MIT",
|
|
@@ -147,6 +147,7 @@
|
|
|
147
147
|
"files": [
|
|
148
148
|
"dist",
|
|
149
149
|
"src",
|
|
150
|
+
"!src/lab",
|
|
150
151
|
"docs",
|
|
151
152
|
"tailwind.config.js"
|
|
152
153
|
],
|
|
@@ -10,7 +10,8 @@ export interface ExerciseHeadingProps {
|
|
|
10
10
|
/** Prescription line values (sets × reps @ load). */
|
|
11
11
|
sets: number
|
|
12
12
|
reps: number | string
|
|
13
|
-
load
|
|
13
|
+
/** Load value; a string (e.g. "—") passes through verbatim for an unset/discovery load. */
|
|
14
|
+
load: number | string
|
|
14
15
|
unit?: 'lbs' | 'kg'
|
|
15
16
|
/** Tempo tuple [eccentric, pauseBottom, concentric, pauseTop]; hidden when absent. */
|
|
16
17
|
tempo?: [number, number, number, number]
|
|
@@ -26,10 +27,11 @@ function headingLabel(
|
|
|
26
27
|
name: string,
|
|
27
28
|
sets: number,
|
|
28
29
|
reps: number | string,
|
|
29
|
-
load: number,
|
|
30
|
+
load: number | string,
|
|
30
31
|
unit: string
|
|
31
32
|
) {
|
|
32
|
-
|
|
33
|
+
const loadLabel = typeof load === 'number' ? roundWeight(load) : load
|
|
34
|
+
return `${name}, ${sets}×${reps} @ ${loadLabel} ${unit}`
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
/**
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
2
|
import { View, Text, type ViewProps } from 'react-native'
|
|
3
|
-
import { primitiveColors } from '../../../theme/tokens/primitives'
|
|
4
3
|
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
5
4
|
import { Typography } from '../Typography'
|
|
6
5
|
import { TimerReadout } from '../TimerReadout'
|
|
@@ -11,9 +10,11 @@ import { paceTone, paceToneColor } from './paceTone'
|
|
|
11
10
|
|
|
12
11
|
const t = getSemanticColors('dark')
|
|
13
12
|
|
|
14
|
-
// The
|
|
15
|
-
// the
|
|
16
|
-
|
|
13
|
+
// The header shares the SideNav's `background-base` (charcoal 900, #101010) so the nav and
|
|
14
|
+
// the rail header read as ONE continuous dark plane on the left — the sunk exercise list
|
|
15
|
+
// (charcoal 800) sits raised off it. Previously charcoal 400 (#1F1F1F), which made the header
|
|
16
|
+
// the lightest rail surface and let it blend with the rows instead of anchoring to the nav.
|
|
17
|
+
const HEADER_BG = t['background-base'] // #101010 — matches SideNav bg-background-base
|
|
17
18
|
|
|
18
19
|
// The chunked pace bar matches the SetStrip language but sits tighter than the default
|
|
19
20
|
// SetStrip gap (5) — the locked design uses a 3px gap and a 9px track.
|
|
@@ -86,7 +87,7 @@ export function SessionHeader({
|
|
|
86
87
|
className={className}
|
|
87
88
|
style={[
|
|
88
89
|
{
|
|
89
|
-
backgroundColor:
|
|
90
|
+
backgroundColor: HEADER_BG,
|
|
90
91
|
paddingTop: 11,
|
|
91
92
|
paddingRight: 12,
|
|
92
93
|
paddingBottom: 12,
|
|
@@ -8,11 +8,14 @@ import type { MetricTileData } from './MetricTiles'
|
|
|
8
8
|
import type { SetStripSet } from './SetStrip'
|
|
9
9
|
import type { ExerciseIndicatorKind } from './ExerciseIndicator'
|
|
10
10
|
|
|
11
|
-
// Charcoal-ramp surfaces (the locked S3 shell shades): the nav + the heading plane
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
// Charcoal-ramp surfaces (the locked S3 shell shades): the nav + the heading plane read as
|
|
12
|
+
// ONE dark plane (charcoal 900, #101010, set in SessionHeader). The exercise list is a
|
|
13
|
+
// LIGHTER inset panel (charcoal 600) — recessed via its inner shadow yet paler than the
|
|
14
|
+
// header, so the transparent exercise headings on it never blend into the header plane.
|
|
15
|
+
// (Band-aid: the list is picked to clear the components; the durable fix is a `<Surface>`
|
|
16
|
+
// primitive whose cards own their own raised background — see the rail's PR notes.)
|
|
17
|
+
const INSET = primitiveColors.charcoal[600] // #191919 — lighter sunk exercise-list panel
|
|
18
|
+
const DIVIDER = primitiveColors.charcoal[300] // #2C2C2C — row divider (raised to read on #191919)
|
|
16
19
|
|
|
17
20
|
const DEFAULT_WIDTH = 246
|
|
18
21
|
|
|
@@ -20,7 +23,15 @@ export interface SessionRailExercise {
|
|
|
20
23
|
/** Stable key + press payload. Falls back to list index when absent. */
|
|
21
24
|
id?: string
|
|
22
25
|
name: string
|
|
23
|
-
|
|
26
|
+
/** `weight` accepts a string (e.g. "—") for an unset/discovery load — never a faked 0. */
|
|
27
|
+
summary: { sets: number; reps: number | string; weight: number | string; unit: 'lbs' | 'kg' }
|
|
28
|
+
/**
|
|
29
|
+
* Prescribed set count for the header total + pace-bar segment width. Distinct from
|
|
30
|
+
* `summary.sets`, which is the row's live DONE count (0 for the active exercise before
|
|
31
|
+
* its first set) — using that for the total under-counts by the active exercise's
|
|
32
|
+
* remaining sets. Falls back to `summary.sets` when absent.
|
|
33
|
+
*/
|
|
34
|
+
plannedSets?: number
|
|
24
35
|
tempo?: [number, number, number, number]
|
|
25
36
|
indicator?: ExerciseIndicatorKind
|
|
26
37
|
/** Per-set performance data driving the heading strip. */
|
|
@@ -84,7 +95,7 @@ export function SessionRail({
|
|
|
84
95
|
>
|
|
85
96
|
<SessionHeader
|
|
86
97
|
title={title}
|
|
87
|
-
plan={exercises.map((ex) => ({ name: ex.name, sets: ex.summary.sets }))}
|
|
98
|
+
plan={exercises.map((ex) => ({ name: ex.name, sets: ex.plannedSets ?? ex.summary.sets }))}
|
|
88
99
|
setsDone={setsDone}
|
|
89
100
|
elapsedMs={elapsedMs}
|
|
90
101
|
budgetMs={budgetMs}
|
|
@@ -11,7 +11,8 @@ const AT = '@'
|
|
|
11
11
|
export interface SetsRepsLoadProps extends ViewProps {
|
|
12
12
|
sets: number
|
|
13
13
|
reps: number | string
|
|
14
|
-
load
|
|
14
|
+
/** Load value; a string (e.g. "—") passes through verbatim for an unset/discovery load. */
|
|
15
|
+
load: number | string
|
|
15
16
|
/** Displayed load unit label (e.g. "lb", "kg"). */
|
|
16
17
|
unit?: string
|
|
17
18
|
/** Cell font size (px). Default 11 — the compact rail/card scale. Raise for a wall read-out. */
|
|
@@ -58,15 +58,13 @@ export function DashboardShell({
|
|
|
58
58
|
className,
|
|
59
59
|
}: DashboardShellProps) {
|
|
60
60
|
return (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
onSelectDevice={onSelectDevice}
|
|
69
|
-
/>
|
|
61
|
+
// Column: the TopBar spans the FULL width across the top, and the SideNav sits BELOW it
|
|
62
|
+
// in the content row (not a full-height left rail). This keeps the brand/status band
|
|
63
|
+
// unbroken edge-to-edge and lets the nav align under it.
|
|
64
|
+
<View className={cn('flex-1 bg-surface-base', className)}>
|
|
65
|
+
<TopBar state={state} devices={devices} subtitle={subtitle} onSelectDevice={onSelectDevice} />
|
|
66
|
+
<View className="flex-1 flex-row">
|
|
67
|
+
<SideNav activeKey={activeKey} items={navItems} liveKey={liveKey} onNavigate={onNavigate} />
|
|
70
68
|
<View className="flex-1">{children ?? <ContentPlaceholder />}</View>
|
|
71
69
|
</View>
|
|
72
70
|
</View>
|
|
@@ -65,7 +65,12 @@ export function NavItem({
|
|
|
65
65
|
<Typography
|
|
66
66
|
variant="button"
|
|
67
67
|
color="inherit"
|
|
68
|
-
|
|
68
|
+
// fontSize inline, not `text-[8.5px]`: the `button` variant's `text-sm` (14px) and the
|
|
69
|
+
// arbitrary `text-[8.5px]` are a same-property class conflict that the consumer's
|
|
70
|
+
// Tailwind build resolved the wrong way (labels rendered ~14px on the wall). Inline
|
|
71
|
+
// size wins unambiguously; className keeps weight/case/tracking.
|
|
72
|
+
style={{ fontSize: 8.5, lineHeight: 11 }}
|
|
73
|
+
className={cn('font-bold uppercase tracking-[0.4px]', labelColor)}
|
|
69
74
|
>
|
|
70
75
|
{label}
|
|
71
76
|
</Typography>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/* eslint-disable titan/no-device-internals --
|
|
2
|
+
* This file is the rule's own test bench, so it necessarily contains the shapes
|
|
3
|
+
* the rule rejects. Every value below is invented for the test — none is a real
|
|
4
|
+
* device value.
|
|
5
|
+
*/
|
|
6
|
+
import { RuleTester } from 'eslint'
|
|
7
|
+
import rule from '../../eslint-rules/no-device-internals'
|
|
8
|
+
|
|
9
|
+
const ruleTester = new RuleTester({
|
|
10
|
+
languageOptions: { ecmaVersion: 2022, sourceType: 'module' },
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
describe('no-device-internals', () => {
|
|
14
|
+
it('flags device internals without flagging design-system content', () => {
|
|
15
|
+
ruleTester.run('no-device-internals', rule as never, {
|
|
16
|
+
valid: [
|
|
17
|
+
// Hex colours are the dominant hex form in this package.
|
|
18
|
+
{ code: "const bg = '#1C1C1C'" },
|
|
19
|
+
{ code: "const bg = '#fff'" },
|
|
20
|
+
{ code: 'const bg = `${base} #1C1C1CFF`' },
|
|
21
|
+
// Dimension and ratio strings: the `\b` guard before `0x` means a digit
|
|
22
|
+
// followed by `x` never reads as a hex literal. These exact strings
|
|
23
|
+
// tripped an earlier, looser sweep.
|
|
24
|
+
{ code: "// icons render at 20x20" },
|
|
25
|
+
{ code: "// 200x400 intrinsic -> ~160x320 px" },
|
|
26
|
+
// SVG path coordinates — digit-only runs are not byte frames.
|
|
27
|
+
{ code: 'const d = "M12 2 22 12 12 22 2 12Z"' },
|
|
28
|
+
{ code: 'const d = "M7 7 17 17 12 22 12 2 17 7 7 17"' },
|
|
29
|
+
// Timestamps — `-` and `:` are not byte separators.
|
|
30
|
+
{ code: "// datetime // 2024-01-15 14:30" },
|
|
31
|
+
// "cascade" in its CSS sense is ordinary styling vocabulary.
|
|
32
|
+
{ code: "// RN doesn't cascade text color, so hand-color the text" },
|
|
33
|
+
// Plain numbers and durations.
|
|
34
|
+
{ code: 'const duration = 300' },
|
|
35
|
+
],
|
|
36
|
+
invalid: [
|
|
37
|
+
// Bare hex identifier, in code and in a comment.
|
|
38
|
+
{ code: 'const mode = 0xab', errors: [{ messageId: 'hex' }] },
|
|
39
|
+
{ code: '// resistance mode (cmd=0xab)', errors: [{ messageId: 'hex' }] },
|
|
40
|
+
{ code: "const s = 'send cmd=0xab now'", errors: [{ messageId: 'hex' }] },
|
|
41
|
+
// Raw byte frame: 4+ space-separated pairs including hex letters.
|
|
42
|
+
{ code: '// frame: ab cd ef 01', errors: [{ messageId: 'frame' }] },
|
|
43
|
+
// Transport UUID.
|
|
44
|
+
{
|
|
45
|
+
code: "const id = '0000fe59-1234-5678-9abc-def012345678'",
|
|
46
|
+
errors: [{ messageId: 'uuid' }],
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
|
-
import { ScrollView, View } from 'react-native'
|
|
3
|
-
import { SessionRail } from '../../components'
|
|
4
|
-
import { ExerciseHeader, LiveView } from './LiveView'
|
|
5
|
-
import { RestView } from './RestView'
|
|
6
|
-
import {
|
|
7
|
-
dashboardFixture,
|
|
8
|
-
deriveDualModel,
|
|
9
|
-
deriveRailExercises,
|
|
10
|
-
type DashboardModel,
|
|
11
|
-
} from './fixtures'
|
|
12
|
-
|
|
13
|
-
export type LivePageVariant = 'live' | 'live-dual' | 'rest'
|
|
14
|
-
|
|
15
|
-
/** Floor the live panel around phone width so it stops collapsing on a narrow window. */
|
|
16
|
-
const PANEL_MIN_WIDTH = 390
|
|
17
|
-
|
|
18
|
-
export interface LivePageProps {
|
|
19
|
-
/** Which stage to show in the main region. */
|
|
20
|
-
variant?: LivePageVariant
|
|
21
|
-
/** The dashboard store snapshot. Defaults to the built-in fixture. */
|
|
22
|
-
model?: DashboardModel
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Lab specimen — the North Star wall-dashboard CONTENT (mounts inside `DashboardShell`'s
|
|
27
|
-
* children slot): the persistent {@link SessionRail} context beside a swappable stage
|
|
28
|
-
* ({@link LiveView} mid-set / {@link RestView} between sets).
|
|
29
|
-
*
|
|
30
|
-
* `live-dual` renders a dual-mode (bilateral) exercise as TWO stacked {@link LiveView}
|
|
31
|
-
* layers — one per voltra (left/right) — the v1 treatment for two-device sets. A
|
|
32
|
-
* unified split-bar view is a later exploration.
|
|
33
|
-
*
|
|
34
|
-
* NOT a published component — lab-scoped composition of production primitives only.
|
|
35
|
-
* The rail footer pace read-out is intentionally OMITTED (NO-DATA — no store field yet).
|
|
36
|
-
*/
|
|
37
|
-
export function LivePage({ variant = 'live', model = dashboardFixture }: LivePageProps) {
|
|
38
|
-
const exercises = deriveRailExercises(model)
|
|
39
|
-
const completedSets = model.session.completedSets.length
|
|
40
|
-
const isLive = variant === 'live' || variant === 'live-dual'
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
<View className="flex-1 flex-row bg-surface-base">
|
|
44
|
-
<SessionRail
|
|
45
|
-
title={model.session.title}
|
|
46
|
-
exercises={exercises}
|
|
47
|
-
setsDone={isLive ? completedSets + 0.75 : completedSets}
|
|
48
|
-
elapsedMs={18 * 60_000}
|
|
49
|
-
budgetMs={45 * 60_000}
|
|
50
|
-
running={isLive}
|
|
51
|
-
width={272}
|
|
52
|
-
metrics={[
|
|
53
|
-
{ label: 'Volume', value: '76%' },
|
|
54
|
-
{ label: 'Load', value: '7.3k' },
|
|
55
|
-
{ label: 'Fatigue', value: 'MOD' },
|
|
56
|
-
]}
|
|
57
|
-
/>
|
|
58
|
-
{/* footer pace strip — STUB / NO-DATA: intentionally omitted (no store field yet). */}
|
|
59
|
-
|
|
60
|
-
{/* Panel floors at ~phone width so the live view stops collapsing; rail-aware
|
|
61
|
-
breakpoints below this are a later pass. */}
|
|
62
|
-
<View className="flex-1" style={{ minWidth: PANEL_MIN_WIDTH }}>
|
|
63
|
-
{/* workout title + targets — page-level, always visible, independent of single/dual. */}
|
|
64
|
-
<ExerciseHeader session={model.session} />
|
|
65
|
-
<View className="flex-1">
|
|
66
|
-
{variant === 'live-dual' ? (
|
|
67
|
-
<DualLiveStage model={model} />
|
|
68
|
-
) : variant === 'live' ? (
|
|
69
|
-
// `slot` names the active voltra — the shell has two connected, so the live view
|
|
70
|
-
// flags which one it is reading from (the multi-device single-view case).
|
|
71
|
-
<LiveView model={model} slot="L" />
|
|
72
|
-
) : (
|
|
73
|
-
<RestView model={model} />
|
|
74
|
-
)}
|
|
75
|
-
</View>
|
|
76
|
-
</View>
|
|
77
|
-
</View>
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Dual-mode stage: two stacked live layers, one per voltra (left dominant, right lags).
|
|
83
|
-
*
|
|
84
|
-
* The stage SCROLLS so two full live read-outs never clip on a height-restricted wall.
|
|
85
|
-
* Each layer also compresses its height-drivers (shorter hero, tighter gaps — see
|
|
86
|
-
* {@link LiveView}'s `side`/`dual` handling) so a tall wall fits both with little or no
|
|
87
|
-
* scroll; a trimmed-per-layer treatment is a later option if scrolling proves awkward.
|
|
88
|
-
*/
|
|
89
|
-
function DualLiveStage({ model }: { model: DashboardModel }) {
|
|
90
|
-
const { left, right } = deriveDualModel(model)
|
|
91
|
-
return (
|
|
92
|
-
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ flexGrow: 1 }}>
|
|
93
|
-
<View style={{ flex: 1, minHeight: 340 }}>
|
|
94
|
-
<LiveView model={left} side="left" />
|
|
95
|
-
</View>
|
|
96
|
-
<View style={{ flex: 1, minHeight: 340 }}>
|
|
97
|
-
<LiveView model={right} side="right" />
|
|
98
|
-
</View>
|
|
99
|
-
</ScrollView>
|
|
100
|
-
)
|
|
101
|
-
}
|