@humanforest/charts 0.5.0 → 0.5.1

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/CHANGELOG.md CHANGED
@@ -2,6 +2,52 @@
2
2
 
3
3
  Every @humanforest package shares one version, so one entry covers all seven packages. Newest first.
4
4
 
5
+ ## 0.5.1 — 2026-11-03
6
+
7
+ ### Features
8
+
9
+ - **ui**: add FToggle and FToggleGroup
10
+ - **docs**: add screen options and a width handle to recipe previews
11
+ - **ui**: add FPasswordInput, a password field with a show/hide toggle
12
+ - **ui**: show a bay's fill on the place mark
13
+ - **recipes**: preview web recipes at phone, tablet or any dragged width
14
+ - **charts,ui**: let chart frames fill the height their card gives them
15
+ - **ui**: let an inset card's action sit in its header row
16
+ - **ui**: one neutral disc for every vehicle marker, also when zoomed out
17
+ - **ui**: marker tasks, selection, live pulse and FPlaceMark
18
+ - **ui**: update the vehicle marker glyphs
19
+
20
+ ### Fixes
21
+
22
+ - **docs**: let a recipe's live screen scroll
23
+ - **docs**: show each recipe's name once on the index
24
+ - **ui**: give FPlaceMark the place asset's P and house
25
+ - **ui**: give FAppShell a main landmark and a skip link
26
+ - **docs**: keep the battery-swap bike-ID field at 16px so iOS doesn't zoom
27
+ - **ui**: give md buttons the md fields' 16px text on touch
28
+ - **ui**: match md select triggers to the 16px fields on touch
29
+ - **ui**: give each vehicle marker its state colour again
30
+ - **recipes**: size the phone screens for the thumb and the glance
31
+ - **recipes**: make the ops screens dense, ranked and full height
32
+ - **recipes**: put the form and state screens on one heading scale
33
+ - **recipes**: anchor the auth screens and tighten their hierarchy
34
+ - **recipes**: move "Open full screen" above the preview
35
+ - **recipes**: size phone specimens to the device frame
36
+ - **recipes**: give the code and reset screens the sign-in frame
37
+ - **recipes**: keep labels and warnings legible and every control visible
38
+ - **recipes**: take state labels and battery tone from the platform
39
+ - **recipes**: keep the phone flows doing what their comments argue
40
+ - **recipes**: make the form recipes validate and reset as they claim
41
+ - support exact sidebar route matching (#10)
42
+ - **ui**: keep md text fields at 16px on touch so iOS doesn't zoom
43
+ - **ui**: let a tap through the accuracy ring too
44
+ - **ui**: let a tap through the position dot
45
+ - **ui**: keep the position dot above the map's markers
46
+
47
+ ### Other changes
48
+
49
+ - Add renovate.json
50
+
5
51
  ## 0.5.0 — 2026-10-25
6
52
 
7
53
  ### Breaking changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanforest/charts",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -29,7 +29,7 @@
29
29
  "vue": "^3.5.22"
30
30
  },
31
31
  "dependencies": {
32
- "@humanforest/tokens": "^0.5.0"
32
+ "@humanforest/tokens": "^0.5.1"
33
33
  },
34
34
  "publishConfig": {
35
35
  "access": "public"
@@ -20,6 +20,7 @@ import { chartLabel, fromCategories } from './fromCategories';
20
20
  import { crosshairConfig } from './crosshairConfig';
21
21
  import { forestTooltip, type ForestTooltipOptions } from './forestTooltip';
22
22
  import { motionDuration } from './motionDuration';
23
+ import { frameHeight, type FrameHeight } from './frameHeight';
23
24
  import { indexAxes, indexContainer } from './frameIndex';
24
25
  import { tooltipDefaults, tooltipContainer } from './presets';
25
26
  import { useThemeVersion } from './useThemeVersion';
@@ -32,7 +33,9 @@ const props = withDefaults(
32
33
  data: Datum[];
33
34
  /** Palette categories (from useChartPalette) — series order, names, colours. */
34
35
  categories: Record<string, BulletLegendItemInterface>;
35
- height?: number;
36
+ /** The chart's height in px, or `fill` to take the height of the frame's container, floored at
37
+ * FILL_MIN_HEIGHT. For a chart in a stretched card, which the card's row makes taller. */
38
+ height?: FrameHeight;
36
39
  /** Numeric field to plot on x (e.g. a timestamp). Default: the row index. */
37
40
  xKey?: keyof Datum & string;
38
41
  /** Blind spread onto VisXYContainer — any XYContainerConfigInterface key, presets last-wins. */
@@ -133,6 +136,9 @@ useTooltipSnap(rootEl, tipId, props.crosshair ? 'crosshair' : 'mark', () => !pro
133
136
  // propagates to marks and axes. Override via the prop or container.duration.
134
137
  const resolvedDuration = computed(() => props.duration ?? motionDuration());
135
138
 
139
+ // The rules for `fill` live in frameHeight.ts; a number reaches the container exactly as before.
140
+ const sizing = computed(() => frameHeight(props.height));
141
+
136
142
  // Theme flips and palette identity changes both remount: Unovis caches resolved colours on mount.
137
143
  const themeVersion = useThemeVersion();
138
144
  const repaintKey = computed(() =>
@@ -146,33 +152,37 @@ const repaintKey = computed(() =>
146
152
  role="img"
147
153
  :aria-label="chartLabel(categories, ariaLabel)"
148
154
  style="display: flex; flex-direction: column; gap: 0.75rem"
155
+ :style="sizing.root"
149
156
  >
150
- <VisXYContainer
151
- :key="repaintKey"
152
- :data="data"
153
- :height="height"
154
- :duration="resolvedDuration"
155
- v-bind="{ ...containerConfig, ...$attrs }"
156
- >
157
- <slot name="mark" :data="data" :x="acc.xAccessor" :y="acc.yAccessors" :colors="acc.colors" />
158
- <VisAxis v-if="axisX !== false" v-bind="axes.axisX" />
159
- <VisAxis v-if="axisY !== false" v-bind="axes.axisY" />
160
- <template v-if="tooltip !== false">
161
- <!-- duration 0 is deliberate, and load-bearing twice over. Unovis animates the crosshair
162
- line's x through a transition, so it eases toward the row the pointer just snapped to
163
- — which reads as lag on the one element that should track the pointer exactly, and
164
- leaves the line's real position mid-flight while the snap is reading it as the
165
- tooltip's anchor. Easing it would put the panel on the wrong side of the plot. -->
166
- <VisCrosshair
167
- v-if="crosshair"
168
- ref="crosshairRef"
169
- v-bind="crosshairProps"
170
- :template="tipFn"
171
- :duration="0"
172
- />
173
- <VisTooltip v-bind="tooltipConfig" />
174
- </template>
175
- </VisXYContainer>
157
+ <div :style="sizing.plot">
158
+ <VisXYContainer
159
+ :key="repaintKey"
160
+ :data="data"
161
+ :height="sizing.container"
162
+ :duration="resolvedDuration"
163
+ :style="sizing.host"
164
+ v-bind="{ ...containerConfig, ...$attrs }"
165
+ >
166
+ <slot name="mark" :data="data" :x="acc.xAccessor" :y="acc.yAccessors" :colors="acc.colors" />
167
+ <VisAxis v-if="axisX !== false" v-bind="axes.axisX" />
168
+ <VisAxis v-if="axisY !== false" v-bind="axes.axisY" />
169
+ <template v-if="tooltip !== false">
170
+ <!-- duration 0 is deliberate, and load-bearing twice over. Unovis animates the crosshair
171
+ line's x through a transition, so it eases toward the row the pointer just snapped to
172
+ — which reads as lag on the one element that should track the pointer exactly, and
173
+ leaves the line's real position mid-flight while the snap is reading it as the
174
+ tooltip's anchor. Easing it would put the panel on the wrong side of the plot. -->
175
+ <VisCrosshair
176
+ v-if="crosshair"
177
+ ref="crosshairRef"
178
+ v-bind="crosshairProps"
179
+ :template="tipFn"
180
+ :duration="0"
181
+ />
182
+ <VisTooltip v-bind="tooltipConfig" />
183
+ </template>
184
+ </VisXYContainer>
185
+ </div>
176
186
  <VisBulletLegend
177
187
  v-if="legend"
178
188
  :items="acc.legendItems"
@@ -10,6 +10,7 @@ import type { BulletLegendItemInterface, TooltipConfigInterface } from '@unovis/
10
10
  // self-import through it would create a module cycle.
11
11
  import { chartLabel, fromCategories } from './fromCategories';
12
12
  import { motionDuration } from './motionDuration';
13
+ import { frameHeight, type FrameHeight } from './frameHeight';
13
14
  import { tooltipDefaults, tooltipContainer } from './presets';
14
15
  import { useThemeVersion } from './useThemeVersion';
15
16
  import { useTooltipSnap, nextTipId } from './snapTooltip';
@@ -21,7 +22,9 @@ const props = withDefaults(
21
22
  data: Data;
22
23
  /** Optional: series colours + legend. Omit for charts that label directly (Sankey, Graph). */
23
24
  categories?: Record<string, BulletLegendItemInterface>;
24
- height?: number;
25
+ /** The chart's height in px, or `fill` to take the height of the frame's container, floored at
26
+ * FILL_MIN_HEIGHT — the same contract as FChartFrame's. */
27
+ height?: FrameHeight;
25
28
  /** Rendered only when given — single-container tooltips need `triggers` to show anything. */
26
29
  tooltip?: Partial<TooltipConfigInterface>;
27
30
  /** Let the panel track the pointer instead of snapping to the hovered mark. Off by default:
@@ -66,6 +69,7 @@ const tooltipConfig = computed<Partial<TooltipConfigInterface>>(() => ({
66
69
  const rootEl = ref<HTMLElement>();
67
70
  useTooltipSnap(rootEl, tipId, 'mark', () => !props.followCursor);
68
71
  const resolvedDuration = computed(() => props.duration ?? motionDuration());
72
+ const sizing = computed(() => frameHeight(props.height));
69
73
 
70
74
  const themeVersion = useThemeVersion();
71
75
  const repaintKey = computed(() =>
@@ -79,17 +83,21 @@ const repaintKey = computed(() =>
79
83
  role="img"
80
84
  :aria-label="chartLabel(categories, ariaLabel)"
81
85
  style="display: flex; flex-direction: column; gap: 0.75rem"
86
+ :style="sizing.root"
82
87
  >
83
- <VisSingleContainer
84
- :key="repaintKey"
85
- :data="data"
86
- :height="height"
87
- :duration="resolvedDuration"
88
- v-bind="$attrs"
89
- >
90
- <slot name="mark" :data="data" :colors="acc.colors" />
91
- <VisTooltip v-if="tooltip" v-bind="tooltipConfig" />
92
- </VisSingleContainer>
88
+ <div :style="sizing.plot">
89
+ <VisSingleContainer
90
+ :key="repaintKey"
91
+ :data="data"
92
+ :height="sizing.container"
93
+ :duration="resolvedDuration"
94
+ :style="sizing.host"
95
+ v-bind="$attrs"
96
+ >
97
+ <slot name="mark" :data="data" :colors="acc.colors" />
98
+ <VisTooltip v-if="tooltip" v-bind="tooltipConfig" />
99
+ </VisSingleContainer>
100
+ </div>
93
101
  <VisBulletLegend
94
102
  v-if="legend && acc.legendItems.length"
95
103
  :items="acc.legendItems"
@@ -0,0 +1,43 @@
1
+ // How a frame's `height` reaches the chart. A number is the container's own height in px, handed to
2
+ // Unovis as it always was. `fill` takes the height of the frame's own container instead, through CSS
3
+ // alone: the root takes 100% of its parent, the plot box takes what the legend leaves, and the Unovis
4
+ // host is laid over that box. Unovis measures the host when it renders, and its own ResizeObserver
5
+ // redraws the chart when the box changes size, so the frame adds no resize handling of its own.
6
+ //
7
+ // ★ THE HOST IS LAID OVER THE BOX, NOT IN ITS FLOW. A Unovis container with no `height` measures its
8
+ // host, and an in-flow host is as tall as the SVG inside it: the SVG sizes the host and the host sizes
9
+ // the SVG, so the chart renders at Unovis's 300px fallback and stays there. Laid over the box, the
10
+ // host adds nothing to the layout and the box's height comes from the layout alone. Inline styles,
11
+ // not utilities: Unovis's stylesheet pins its containers to `position: relative`, and a class loses.
12
+ //
13
+ // Under a number the box is `display: contents`, which draws no box at all, so the host stays the
14
+ // root's flex item exactly as it was before `fill` existed.
15
+
16
+ export type FrameHeight = number | 'fill';
17
+
18
+ /**
19
+ * The plot's floor under `fill`, in px. It holds when the container is shorter, and it is the whole
20
+ * height when the container has none of its own (a block in normal flow), so an unconstrained `fill`
21
+ * chart draws at the floor rather than collapsing to nothing. 160 sits under both frames' numeric
22
+ * defaults (280 and 260), so a `fill` chart never asks its row for more height than a numeric one
23
+ * would, and it keeps a readable band of plot between an XY chart's axes.
24
+ */
25
+ export const FILL_MIN_HEIGHT = 160;
26
+
27
+ type Style = Record<string, string>;
28
+
29
+ /** What each layer of the frame gets: the container's `height` prop, and the root, plot box and host styles. */
30
+ export function frameHeight(height: FrameHeight): {
31
+ container: number | undefined;
32
+ root: Style | undefined;
33
+ plot: Style;
34
+ host: Style | undefined;
35
+ } {
36
+ if (height !== 'fill') return { container: height, root: undefined, plot: { display: 'contents' }, host: undefined };
37
+ return {
38
+ container: undefined,
39
+ root: { height: '100%' },
40
+ plot: { position: 'relative', flex: '1 1 0', minHeight: `${FILL_MIN_HEIGHT}px` },
41
+ host: { position: 'absolute', inset: '0' },
42
+ };
43
+ }
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  // Importing this barrel needs an SFC pipeline (Vite/Nuxt) — everything here is browser-bound.
5
5
  export { default as FChartFrame } from './FChartFrame.vue';
6
6
  export { default as FChartFrameSingle } from './FChartFrameSingle.vue';
7
+ export { FILL_MIN_HEIGHT, type FrameHeight } from './frameHeight';
7
8
  export { default as FChartLegend } from './FChartLegend.vue';
8
9
  // Word-sized graphic for a KPI tile's `visual` slot: shape only, no axes, no tooltip.
9
10
  export { default as FSparkline } from './FSparkline.vue';