@adea-ai/ui 0.100.2 → 0.101.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.
Files changed (116) hide show
  1. package/dist/components/composites/about-dialog/about-dialog.d.ts.map +1 -1
  2. package/dist/components/composites/about-dialog/about-dialog.js +2 -1
  3. package/dist/components/composites/about-dialog/about-dialog.js.map +1 -1
  4. package/dist/components/composites/account-menu/account-menu.d.ts +7 -0
  5. package/dist/components/composites/account-menu/account-menu.d.ts.map +1 -1
  6. package/dist/components/composites/account-menu/account-menu.js +116 -89
  7. package/dist/components/composites/account-menu/account-menu.js.map +1 -1
  8. package/dist/components/composites/help-center/help-center.d.ts.map +1 -1
  9. package/dist/components/composites/help-center/help-center.js +26 -21
  10. package/dist/components/composites/help-center/help-center.js.map +1 -1
  11. package/dist/components/composites/settings/settings.d.ts.map +1 -1
  12. package/dist/components/composites/settings/settings.js +43 -36
  13. package/dist/components/composites/settings/settings.js.map +1 -1
  14. package/dist/components/composites/stat/stat.d.ts.map +1 -1
  15. package/dist/components/composites/stat/stat.js +46 -35
  16. package/dist/components/composites/stat/stat.js.map +1 -1
  17. package/dist/components/composites/update-dialog/update-dialog.d.ts.map +1 -1
  18. package/dist/components/composites/update-dialog/update-dialog.js +73 -58
  19. package/dist/components/composites/update-dialog/update-dialog.js.map +1 -1
  20. package/dist/components/layout/page/page.d.ts.map +1 -1
  21. package/dist/components/layout/page/page.js +19 -13
  22. package/dist/components/layout/page/page.js.map +1 -1
  23. package/dist/components/theme/appearance-panel.d.ts.map +1 -1
  24. package/dist/components/theme/appearance-panel.js +37 -18
  25. package/dist/components/theme/appearance-panel.js.map +1 -1
  26. package/dist/components/ui/alert-dialog/alert-dialog.d.ts.map +1 -1
  27. package/dist/components/ui/alert-dialog/alert-dialog.js +5 -1
  28. package/dist/components/ui/alert-dialog/alert-dialog.js.map +1 -1
  29. package/dist/components/ui/card/card.d.ts.map +1 -1
  30. package/dist/components/ui/card/card.js +5 -1
  31. package/dist/components/ui/card/card.js.map +1 -1
  32. package/dist/components/ui/chart/chart-options.d.ts +3 -2
  33. package/dist/components/ui/chart/chart-options.d.ts.map +1 -1
  34. package/dist/components/ui/chart/chart-options.js +30 -9
  35. package/dist/components/ui/chart/chart-options.js.map +1 -1
  36. package/dist/components/ui/chart/chart.d.ts +4 -9
  37. package/dist/components/ui/chart/chart.d.ts.map +1 -1
  38. package/dist/components/ui/chart/chart.js +65 -10
  39. package/dist/components/ui/chart/chart.js.map +1 -1
  40. package/dist/components/ui/dialog/dialog.d.ts.map +1 -1
  41. package/dist/components/ui/dialog/dialog.js +5 -1
  42. package/dist/components/ui/dialog/dialog.js.map +1 -1
  43. package/dist/components/ui/drawer/drawer.d.ts.map +1 -1
  44. package/dist/components/ui/drawer/drawer.js +5 -1
  45. package/dist/components/ui/drawer/drawer.js.map +1 -1
  46. package/dist/components/ui/field/field.d.ts.map +1 -1
  47. package/dist/components/ui/field/field.js +2 -1
  48. package/dist/components/ui/field/field.js.map +1 -1
  49. package/dist/components/ui/item/item.d.ts +1 -1
  50. package/dist/components/ui/sheet/sheet.d.ts.map +1 -1
  51. package/dist/components/ui/sheet/sheet.js +5 -1
  52. package/dist/components/ui/sheet/sheet.js.map +1 -1
  53. package/dist/components/ui/typography/index.d.ts +3 -0
  54. package/dist/components/ui/typography/index.d.ts.map +1 -0
  55. package/dist/components/ui/typography/index.js +2 -0
  56. package/dist/components/ui/typography/typography.d.ts +84 -0
  57. package/dist/components/ui/typography/typography.d.ts.map +1 -0
  58. package/dist/components/ui/typography/typography.js +179 -0
  59. package/dist/components/ui/typography/typography.js.map +1 -0
  60. package/dist/index.d.ts +1 -0
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +2 -1
  63. package/dist/r/account-menu.json +1 -0
  64. package/dist/r/alert-dialog.json +1 -0
  65. package/dist/r/card.json +1 -0
  66. package/dist/r/dialog.json +1 -0
  67. package/dist/r/drawer.json +1 -0
  68. package/dist/r/field.json +1 -0
  69. package/dist/r/help-center.json +1 -0
  70. package/dist/r/page.json +1 -0
  71. package/dist/r/registry.json +37 -0
  72. package/dist/r/settings.json +1 -0
  73. package/dist/r/sheet.json +1 -0
  74. package/dist/r/src/components/composites/about-dialog/about-dialog.tsx +3 -2
  75. package/dist/r/src/components/composites/account-menu/account-menu.tsx +105 -66
  76. package/dist/r/src/components/composites/help-center/help-center.tsx +2 -1
  77. package/dist/r/src/components/composites/settings/settings.tsx +4 -1
  78. package/dist/r/src/components/composites/stat/stat.tsx +7 -2
  79. package/dist/r/src/components/composites/update-dialog/update-dialog.tsx +36 -24
  80. package/dist/r/src/components/layout/page/page.tsx +3 -2
  81. package/dist/r/src/components/theme/appearance-panel.tsx +13 -4
  82. package/dist/r/src/components/ui/alert-dialog/alert-dialog.tsx +2 -1
  83. package/dist/r/src/components/ui/card/card.tsx +2 -1
  84. package/dist/r/src/components/ui/chart/chart-options.ts +35 -7
  85. package/dist/r/src/components/ui/chart/chart.tsx +65 -12
  86. package/dist/r/src/components/ui/dialog/dialog.tsx +2 -1
  87. package/dist/r/src/components/ui/drawer/drawer.tsx +2 -1
  88. package/dist/r/src/components/ui/field/field.tsx +2 -3
  89. package/dist/r/src/components/ui/sheet/sheet.tsx +5 -1
  90. package/dist/r/src/components/ui/typography/index.ts +9 -0
  91. package/dist/r/src/components/ui/typography/typography.tsx +205 -0
  92. package/dist/r/stat.json +1 -0
  93. package/dist/r/theming.json +1 -0
  94. package/dist/r/typography.json +24 -0
  95. package/dist/r/update-dialog.json +1 -0
  96. package/package.json +1 -1
  97. package/registry.json +37 -0
  98. package/src/components/composites/about-dialog/about-dialog.tsx +3 -2
  99. package/src/components/composites/account-menu/account-menu.tsx +105 -66
  100. package/src/components/composites/help-center/help-center.tsx +2 -1
  101. package/src/components/composites/settings/settings.tsx +4 -1
  102. package/src/components/composites/stat/stat.tsx +7 -2
  103. package/src/components/composites/update-dialog/update-dialog.tsx +36 -24
  104. package/src/components/layout/page/page.tsx +3 -2
  105. package/src/components/theme/appearance-panel.tsx +13 -4
  106. package/src/components/ui/alert-dialog/alert-dialog.tsx +2 -1
  107. package/src/components/ui/card/card.tsx +2 -1
  108. package/src/components/ui/chart/chart-options.ts +35 -7
  109. package/src/components/ui/chart/chart.tsx +65 -12
  110. package/src/components/ui/dialog/dialog.tsx +2 -1
  111. package/src/components/ui/drawer/drawer.tsx +2 -1
  112. package/src/components/ui/field/field.tsx +2 -3
  113. package/src/components/ui/sheet/sheet.tsx +5 -1
  114. package/src/components/ui/typography/index.ts +9 -0
  115. package/src/components/ui/typography/typography.tsx +205 -0
  116. package/src/index.ts +1 -0
@@ -45,7 +45,30 @@ const furniture = {
45
45
  */
46
46
  export type ChartScaleShape = 'cartesian' | 'radial' | 'none'
47
47
 
48
- const tickFont = { size: 10 }
48
+ /**
49
+ * The chart's type: the UI face from the font axis and a rung of the type scale,
50
+ * declared as tokens like the colours.
51
+ *
52
+ * Chart.js does not inherit the page's font — it paints in its own default
53
+ * family — so every text a chart draws names `--font-sans` explicitly, and the
54
+ * chart follows `data-font` the same way it follows `.dark`. Everything is
55
+ * `--text-2xs`, the rung `ChartLegend` and the frame's metadata already use, so
56
+ * a canvas legend and an HTML one beside it are the same size.
57
+ *
58
+ * Chart.js types `size` as a number of pixels. The token is a `rem` length, so
59
+ * it is written as `var(--text-2xs)` here and the component resolves it to
60
+ * pixels with the colours (see `resolveCssVariables` and `chart.tsx`); the cast
61
+ * is that declaration, not a number.
62
+ */
63
+ function chartFont(weight?: number) {
64
+ return {
65
+ family: 'var(--font-sans)',
66
+ size: 'var(--text-2xs)' as unknown as number,
67
+ ...(weight === undefined ? {} : { weight }),
68
+ }
69
+ }
70
+
71
+ const tickFont = chartFont()
49
72
 
50
73
  function scalesFor(shape: ChartScaleShape): ChartOptions['scales'] {
51
74
  if (shape === 'cartesian') {
@@ -140,7 +163,7 @@ export function chartOptions(
140
163
  boxHeight: 8,
141
164
  usePointStyle: true,
142
165
  pointStyle: 'circle',
143
- font: { size: 11 },
166
+ font: chartFont(),
144
167
  },
145
168
  },
146
169
  tooltip: {
@@ -155,8 +178,9 @@ export function chartOptions(
155
178
  boxWidth: 8,
156
179
  boxHeight: 8,
157
180
  usePointStyle: true,
158
- titleFont: { size: 11, weight: 600 },
159
- bodyFont: { size: 11 },
181
+ titleFont: chartFont(600),
182
+ bodyFont: chartFont(),
183
+ footerFont: chartFont(),
160
184
  },
161
185
  },
162
186
  scales: scalesFor(shape),
@@ -169,17 +193,21 @@ const cssVariable = /^\s*var\(\s*(--[\w-]+)\s*(?:,\s*(.*?))?\s*\)\s*$/
169
193
 
170
194
  /**
171
195
  * Replace every `var(--token)` string in `value` — options, datasets, nested
172
- * arrays — with what `read` returns for the token. A token `read` cannot
196
+ * arrays — with what `read` returns for the token, which may be a number (a
197
+ * length token read as pixels). A token `read` cannot
173
198
  * resolve falls back to the `var()`'s own fallback, then to the string as
174
199
  * written. Functions and class instances pass through untouched, so scriptable
175
200
  * options and gradients keep working.
176
201
  */
177
- export function resolveCssVariables<T>(value: T, read: (name: string) => string | undefined): T {
202
+ export function resolveCssVariables<T>(
203
+ value: T,
204
+ read: (name: string) => string | number | undefined
205
+ ): T {
178
206
  if (typeof value === 'string') {
179
207
  const match = cssVariable.exec(value)
180
208
  if (!match) return value
181
209
  const resolved = read(match[1]!)
182
- if (resolved) return resolved as T
210
+ if (resolved !== undefined && resolved !== '') return resolved as T
183
211
  return (match[2] ? resolveCssVariables(match[2], read) : value) as T
184
212
  }
185
213
  if (Array.isArray(value)) {
@@ -15,7 +15,7 @@ import {
15
15
  RadialLinearScale,
16
16
  Tooltip as ChartTooltip,
17
17
  } from 'chart.js'
18
- import type { ChartOptions, ChartType } from 'chart.js'
18
+ import type { ChartOptions, ChartType, Plugin } from 'chart.js'
19
19
  import { cn } from '#lib/utils'
20
20
  import {
21
21
  chartOptions,
@@ -62,7 +62,9 @@ import {
62
62
  * A canvas cannot read a custom property, so the options and datasets carry
63
63
  * `var(--token)` strings and each chart resolves them against its own element
64
64
  * when it renders, and again whenever the root's attributes change — the
65
- * `.dark` class, `data-accent`, an inline override. See `createThemeResolver`.
65
+ * `.dark` class, `data-accent`, `data-font`, an inline override. The type is
66
+ * resolved the same way: `--font-sans` for the family and a type-scale rung for
67
+ * the size, so a chart follows the font axis too. See `createThemeResolver`.
66
68
  */
67
69
 
68
70
  /**
@@ -119,6 +121,23 @@ function toCanvasColor(value: string): string {
119
121
  return `rgba(${r}, ${g}, ${b}, ${Math.round(((a ?? 255) / 255) * 1000) / 1000})`
120
122
  }
121
123
 
124
+ const length = /^(-?\d*\.?\d+)(px|rem)$/
125
+
126
+ /**
127
+ * A token's computed value as a canvas option: a colour as an sRGB colour, a
128
+ * `px`/`rem` length (a type-scale rung) as the number of pixels Chart.js wants,
129
+ * and anything else — a font stack — as written.
130
+ */
131
+ function toCanvasValue(raw: string): string | number {
132
+ const match = length.exec(raw)
133
+ if (match) {
134
+ const value = Number(match[1])
135
+ if (match[2] === 'px') return value
136
+ return value * (Number.parseFloat(getComputedStyle(document.documentElement).fontSize) || 16)
137
+ }
138
+ return toCanvasColor(raw)
139
+ }
140
+
122
141
  /**
123
142
  * Resolves `var(--token)` strings against a chart's element, and re-resolves
124
143
  * them when the theme changes.
@@ -127,16 +146,25 @@ function toCanvasColor(value: string): string {
127
146
  * inline tokens takes those. The observer watches every attribute on `<html>`,
128
147
  * because that is where both applications switch appearance, accent and theme
129
148
  * (`.dark`, `data-accent`, `data-theme`, inline variables); a theme axis added
130
- * later is covered without touching this. It is released with the chart.
149
+ * later is covered without touching this. It also re-resolves when a web font
150
+ * finishes loading: a canvas draws text once, so a chart rendered before the
151
+ * face selected by `data-font` arrived would otherwise keep the fallback face
152
+ * until something else redrew it. Both are released with the chart.
131
153
  */
132
154
  function createThemeResolver() {
133
155
  let element: HTMLElement | undefined
134
156
  const [revision, invalidate] = createSignal(undefined, { equals: false })
157
+ const onFontsLoaded = () => invalidate()
135
158
 
136
159
  onMount(() => {
137
160
  const observer = new MutationObserver(() => invalidate())
138
161
  observer.observe(document.documentElement, { attributes: true })
139
- onCleanup(() => observer.disconnect())
162
+ const fonts = typeof document.fonts === 'undefined' ? undefined : document.fonts
163
+ fonts?.addEventListener('loadingdone', onFontsLoaded)
164
+ onCleanup(() => {
165
+ observer.disconnect()
166
+ fonts?.removeEventListener('loadingdone', onFontsLoaded)
167
+ })
140
168
  })
141
169
 
142
170
  return {
@@ -147,11 +175,11 @@ function createThemeResolver() {
147
175
  revision()
148
176
  if (!element || typeof getComputedStyle === 'undefined') return value
149
177
  const style = getComputedStyle(element)
150
- const resolved = new Map<string, string | undefined>()
178
+ const resolved = new Map<string, string | number | undefined>()
151
179
  return resolveCssVariables(value, (name) => {
152
180
  if (!resolved.has(name)) {
153
181
  const raw = style.getPropertyValue(name).trim()
154
- resolved.set(name, raw ? toCanvasColor(raw) : undefined)
182
+ resolved.set(name, raw ? toCanvasValue(raw) : undefined)
155
183
  }
156
184
  return resolved.get(name)
157
185
  })
@@ -343,13 +371,36 @@ export function RadarChart(props: SeriesChartProps) {
343
371
  }
344
372
 
345
373
  /**
346
- * A polar area chart: radial bars, for a composition.
374
+ * Puts the themed radial scale back on a chart `solid-chartjs` has just created.
347
375
  *
348
- * Its scale is left to Chart.js's polar-area defaults (`'none'` rather than
349
- * `'radial'`): `solid-chartjs` deletes `options.scales.r` for every chart type
350
- * but radar when it first creates the chart, so radial styling here would apply
351
- * only after the first theme change — a chart that restyles itself on a toggle.
376
+ * `solid-chartjs` deletes `options.scales.r` for every chart type but radar when
377
+ * it first creates a chart. Today that delete lands on a throwaway copy — Solid
378
+ * compiles `options={…}` to a getter, so its next read rebuilds the object with
379
+ * `r` intact — but that is an accident of how the prop is passed: a memoised or
380
+ * static options object would lose the scale, and the polar area would fall back
381
+ * to Chart.js's defaults (grey ticks on a translucent white box, which is a white
382
+ * box on a dark card). Restoring it from this component's own options makes the
383
+ * themed scale independent of both. Later option updates (every theme change)
384
+ * pass `r` through untouched; only creation drops it.
385
+ *
386
+ * `afterInit` runs inside Chart.js's constructor, after the options are read and
387
+ * before the first update builds the scales and draws, so the themed scale is in
388
+ * place for the first frame — no default-styled frame, and no extra update. The
389
+ * plugin is per instance, passed through the chart's `plugins` prop, so nothing
390
+ * is registered globally.
352
391
  */
392
+ function restoreRadialScale(options: () => ChartOptions): Plugin {
393
+ return {
394
+ id: 'adea-radial-scale',
395
+ afterInit(chart) {
396
+ const scale = options().scales?.['r']
397
+ if (!scale) return
398
+ chart.options.scales = { ...chart.options.scales, r: scale }
399
+ },
400
+ }
401
+ }
402
+
403
+ /** A polar area chart: radial bars, for a composition. */
353
404
  export function PolarAreaChart(props: {
354
405
  labels: readonly string[]
355
406
  data: readonly number[]
@@ -357,9 +408,11 @@ export function PolarAreaChart(props: {
357
408
  class?: string
358
409
  }) {
359
410
  const theme = createThemeResolver()
411
+ const options = () => theme.resolve(chartOptions(props.options, 'radial'))
360
412
  return (
361
413
  <div ref={theme.ref} class={cn('h-64', props.class)}>
362
414
  <PolarArea
415
+ plugins={[restoreRadialScale(options)]}
363
416
  data={theme.resolve({
364
417
  labels: [...props.labels],
365
418
  datasets: [
@@ -371,7 +424,7 @@ export function PolarAreaChart(props: {
371
424
  },
372
425
  ],
373
426
  })}
374
- options={theme.resolve(chartOptions(props.options, 'none'))}
427
+ options={options()}
375
428
  />
376
429
  </div>
377
430
  )
@@ -9,6 +9,7 @@ import type { Accessor, ComponentProps, JSX } from 'solid-js'
9
9
  import { createRenderEffect, onCleanup, Show, splitProps } from 'solid-js'
10
10
  import { overlayMotion, overlayPositioner, overlayScrim, overlaySurface } from '#lib/overlay'
11
11
  import { cn } from '#lib/utils'
12
+ import { headingVariants } from '../typography'
12
13
 
13
14
  type FocusCycle = {
14
15
  content: HTMLElement
@@ -327,7 +328,7 @@ export function DialogTitle(props: ComponentProps<typeof KobalteDialog.Title>) {
327
328
  const [local, rest] = splitProps(props, ['class'])
328
329
  return (
329
330
  <KobalteDialog.Title
330
- class={cn('text-base leading-none font-semibold tracking-tight', local.class)}
331
+ class={cn(headingVariants({ size: 'card', leading: 'none' }), local.class)}
331
332
  {...rest}
332
333
  />
333
334
  )
@@ -2,6 +2,7 @@ import DrawerPrimitive from '@corvu/drawer'
2
2
  import type { ComponentProps } from 'solid-js'
3
3
  import { splitProps } from 'solid-js'
4
4
  import { cn } from '#lib/utils'
5
+ import { headingVariants } from '../typography'
5
6
 
6
7
  /**
7
8
  * Drawer.
@@ -96,7 +97,7 @@ export function DrawerTitle(props: ComponentProps<typeof DrawerPrimitive.Label>)
96
97
  const [local, rest] = splitProps(props, ['class'])
97
98
  return (
98
99
  <DrawerPrimitive.Label
99
- class={cn('text-base leading-none font-semibold tracking-tight', local.class)}
100
+ class={cn(headingVariants({ size: 'card', leading: 'none' }), local.class)}
100
101
  {...rest}
101
102
  />
102
103
  )
@@ -4,6 +4,7 @@ import { createUniqueId, Show, splitProps } from 'solid-js'
4
4
  import { Label } from '../label/label'
5
5
  import { createFormFieldContext, FormFieldContext } from '#lib/form-field'
6
6
  import { cn } from '#lib/utils'
7
+ import { headingVariants } from '../typography'
7
8
 
8
9
  /**
9
10
  * Field.
@@ -199,9 +200,7 @@ export function FieldLegend(props: ComponentProps<'legend'> & { variant?: 'legen
199
200
  <legend
200
201
  class={cn(
201
202
  'mb-1',
202
- local.variant === 'label'
203
- ? 'text-sm font-medium'
204
- : 'text-base font-semibold tracking-tight',
203
+ local.variant === 'label' ? 'text-sm font-medium' : headingVariants({ size: 'card' }),
205
204
  local.class
206
205
  )}
207
206
  {...rest}
@@ -10,6 +10,7 @@ import { Show, splitProps } from 'solid-js'
10
10
  import { cva, type VariantProps } from '#lib/variants'
11
11
  import { cn } from '#lib/utils'
12
12
  import { createDialogFocusRestoration, DialogOverlay } from '../dialog/dialog'
13
+ import { headingVariants } from '../typography'
13
14
 
14
15
  /**
15
16
  * Sheet.
@@ -152,7 +153,10 @@ export function SheetFooter(props: ComponentProps<'div'>) {
152
153
  export function SheetTitle(props: ComponentProps<typeof KobalteDialog.Title>) {
153
154
  const [local, rest] = splitProps(props, ['class'])
154
155
  return (
155
- <KobalteDialog.Title class={cn('text-sm leading-none font-semibold', local.class)} {...rest} />
156
+ <KobalteDialog.Title
157
+ class={cn(headingVariants({ size: 'subsection', leading: 'none' }), local.class)}
158
+ {...rest}
159
+ />
156
160
  )
157
161
  }
158
162
 
@@ -0,0 +1,9 @@
1
+ export {
2
+ Heading,
3
+ headingElement,
4
+ headingVariants,
5
+ Text,
6
+ textElement,
7
+ textVariants,
8
+ } from './typography'
9
+ export type { HeadingProps, TextProps } from './typography'
@@ -0,0 +1,205 @@
1
+ import { Polymorphic, type PolymorphicProps } from '@kobalte/core/polymorphic'
2
+ import { type ComponentProps, splitProps, type ValidComponent } from 'solid-js'
3
+ import { cva, type VariantProps } from '#lib/variants'
4
+ import { cn } from '#lib/utils'
5
+
6
+ /**
7
+ * Typography.
8
+ *
9
+ * The type ladder in `theme.css` has eight rungs, each with its own leading. These
10
+ * two components are where a rung is paired with its weight and tracking, so a call
11
+ * site names a *role* — a page title, a section heading, a caption — and never
12
+ * re-decides `text-xl font-semibold tracking-tight` for itself. Twenty call sites
13
+ * that each choose a size and a weight drift; twenty that name a role cannot.
14
+ *
15
+ * **Size is visual; the element is the document outline.** They are separate
16
+ * decisions and the API keeps them separate. Each size has a conventional default
17
+ * element (`display`, `title` and `page` render an `h1`, `section` an `h2`, `card`
18
+ * an `h3`, `subsection` an `h4`) so the common case needs no prop, but the default
19
+ * is a convenience, not a coupling: a card-sized title that is the first heading
20
+ * in a settings section is an `h2`, so it says `as="h2"`. Never pick a `size` to get
21
+ * a heading level, and never pick an element to get a size.
22
+ *
23
+ * Headings are 600, and weights stop at 600 — on a dark surface a bold word blooms.
24
+ * Leading comes from the rung; `leading="none"` is for a title whose surrounding
25
+ * stack already sets the rhythm (a card or dialog header), and is the only other
26
+ * option.
27
+ */
28
+ export const headingVariants = cva('font-semibold', {
29
+ variants: {
30
+ size: {
31
+ /** 30px. An app-level headline — a first-run or empty-app screen. Once per app. */
32
+ display: 'text-3xl tracking-tight',
33
+ /** 24px. A landing title or a headline figure larger than a stat's. */
34
+ title: 'text-2xl tracking-tight',
35
+ /** 20px. The title of a page (`PageHeaderTitle`). */
36
+ page: 'text-xl tracking-tight',
37
+ /** 18px. A section heading inside a page (`PageSection`). */
38
+ section: 'text-lg tracking-tight',
39
+ /** 16px. A card, dialog, drawer or settings-section title. */
40
+ card: 'text-base tracking-tight',
41
+ /** 14px. A heading inside a compact surface: a sheet, a chart, a column. */
42
+ subsection: 'text-sm',
43
+ },
44
+ leading: {
45
+ /** The rung's own line-height from `theme.css`. */
46
+ rung: '',
47
+ /** Line-height 1, for a title whose container's gap sets the rhythm. */
48
+ none: 'leading-none',
49
+ },
50
+ /** Tabular figures, so a row of numbers can be compared without digits shifting. */
51
+ numeric: {
52
+ true: 'tabular-nums',
53
+ },
54
+ tone: {
55
+ /** Inherit the surrounding colour — right on every surface by construction. */
56
+ inherit: '',
57
+ foreground: 'text-foreground',
58
+ muted: 'text-muted-foreground',
59
+ },
60
+ },
61
+ defaultVariants: {
62
+ size: 'section',
63
+ leading: 'rung',
64
+ tone: 'inherit',
65
+ },
66
+ })
67
+
68
+ /**
69
+ * The conventional element for each heading size. Exported so a test (and a
70
+ * reader) can see the whole mapping in one place; pass `as` to depart from it.
71
+ */
72
+ export const headingElement = {
73
+ display: 'h1',
74
+ title: 'h1',
75
+ page: 'h1',
76
+ section: 'h2',
77
+ card: 'h3',
78
+ subsection: 'h4',
79
+ } as const
80
+
81
+ type HeadingVariantProps = VariantProps<typeof headingVariants>
82
+ type HeadingSize = keyof typeof headingElement
83
+
84
+ export type HeadingProps<T extends ValidComponent = 'h2'> = PolymorphicProps<
85
+ T,
86
+ HeadingVariantProps & {
87
+ class?: string
88
+ }
89
+ >
90
+
91
+ export function Heading<T extends ValidComponent = 'h2'>(props: HeadingProps<T>) {
92
+ const [local, rest] = splitProps(props as HeadingProps & { as?: ValidComponent }, [
93
+ 'as',
94
+ 'size',
95
+ 'leading',
96
+ 'numeric',
97
+ 'tone',
98
+ 'class',
99
+ ])
100
+
101
+ return (
102
+ <Polymorphic
103
+ as={local.as ?? headingElement[(local.size ?? 'section') as HeadingSize]}
104
+ data-slot="heading"
105
+ class={cn(
106
+ headingVariants({
107
+ size: local.size,
108
+ leading: local.leading,
109
+ numeric: local.numeric,
110
+ tone: local.tone,
111
+ }),
112
+ local.class
113
+ )}
114
+ {...(rest as ComponentProps<'h2'>)}
115
+ />
116
+ )
117
+ }
118
+
119
+ /**
120
+ * Text.
121
+ *
122
+ * Everything that is not a heading. `text-sm` is the interface default, not 16px,
123
+ * and it carries three weights by role: 400 for body copy, 500 for a label or
124
+ * control text, 600 for emphasis. Below it, `caption` (12px) is metadata and
125
+ * `micro` (11px, 500) is the floor — keyboard keys and the status bar. `code` is
126
+ * the mono face at 12px, for anything a user compares character by character.
127
+ *
128
+ * `tone` is a separate axis from `variant` because "secondary" is a colour
129
+ * decision, not a size: a muted caption and a muted body line are both common.
130
+ * The default inherits, so text on a filled surface picks up that surface's
131
+ * foreground without the caller restating it.
132
+ *
133
+ * Defaults: `body` renders a `p`, `code` a `code`, every other variant a `span`.
134
+ */
135
+ export const textVariants = cva('', {
136
+ variants: {
137
+ variant: {
138
+ /** 14px / 400. Body copy and descriptions. */
139
+ body: 'text-sm',
140
+ /** 14px / 500. Labels, control text, list-row titles. */
141
+ label: 'text-sm font-medium',
142
+ /** 14px / 600. Emphasis inside body copy. */
143
+ strong: 'text-sm font-semibold',
144
+ /** 12px / 400. Metadata, secondary rows, units. */
145
+ caption: 'text-xs',
146
+ /** 11px / 500. The floor: keyboard keys, the status bar. */
147
+ micro: 'text-2xs font-medium',
148
+ /** 12px mono. Ids, paths, hashes, inline code. */
149
+ code: 'font-mono text-xs',
150
+ },
151
+ tone: {
152
+ inherit: '',
153
+ foreground: 'text-foreground',
154
+ muted: 'text-muted-foreground',
155
+ },
156
+ numeric: {
157
+ true: 'tabular-nums',
158
+ },
159
+ },
160
+ defaultVariants: {
161
+ variant: 'body',
162
+ tone: 'inherit',
163
+ },
164
+ })
165
+
166
+ export const textElement = {
167
+ body: 'p',
168
+ label: 'span',
169
+ strong: 'span',
170
+ caption: 'span',
171
+ micro: 'span',
172
+ code: 'code',
173
+ } as const
174
+
175
+ type TextVariantProps = VariantProps<typeof textVariants>
176
+ type TextVariant = keyof typeof textElement
177
+
178
+ export type TextProps<T extends ValidComponent = 'p'> = PolymorphicProps<
179
+ T,
180
+ TextVariantProps & {
181
+ class?: string
182
+ }
183
+ >
184
+
185
+ export function Text<T extends ValidComponent = 'p'>(props: TextProps<T>) {
186
+ const [local, rest] = splitProps(props as TextProps & { as?: ValidComponent }, [
187
+ 'as',
188
+ 'variant',
189
+ 'tone',
190
+ 'numeric',
191
+ 'class',
192
+ ])
193
+
194
+ return (
195
+ <Polymorphic
196
+ as={local.as ?? textElement[(local.variant ?? 'body') as TextVariant]}
197
+ data-slot="text"
198
+ class={cn(
199
+ textVariants({ variant: local.variant, tone: local.tone, numeric: local.numeric }),
200
+ local.class
201
+ )}
202
+ {...(rest as ComponentProps<'p'>)}
203
+ />
204
+ )
205
+ }
package/src/index.ts CHANGED
@@ -74,6 +74,7 @@ export * from './components/ui/toast'
74
74
  export * from './components/ui/toggle'
75
75
  export * from './components/ui/toggle-group'
76
76
  export * from './components/ui/tooltip'
77
+ export * from './components/ui/typography'
77
78
 
78
79
  /* --- Motion -------------------------------------------------------------- */
79
80
  export * from './components/motion'