@refraction-ui/astro 0.18.0 → 0.19.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 (51) hide show
  1. package/dist/action-list/action-list.styles.ts +33 -0
  2. package/dist/action-list/action-list.ts +31 -0
  3. package/dist/action-list/index.ts +16 -0
  4. package/dist/astro-action-list/ActionList.astro +12 -0
  5. package/dist/astro-action-list/ActionListItem.astro +62 -0
  6. package/dist/astro-action-list/index.ts +1 -0
  7. package/dist/astro-callout/Callout.astro +4 -1
  8. package/dist/astro-card/Card.astro +3 -2
  9. package/dist/astro-card/CardTitle.astro +6 -4
  10. package/dist/astro-funnel-chart/FunnelChart.astro +64 -0
  11. package/dist/astro-funnel-chart/index.ts +1 -0
  12. package/dist/astro-line-chart/LineChart.astro +82 -0
  13. package/dist/astro-line-chart/index.ts +1 -0
  14. package/dist/astro-page-header/PageHeader.astro +46 -0
  15. package/dist/astro-page-header/index.ts +1 -0
  16. package/dist/avatar-group/avatar-group.styles.ts +1 -1
  17. package/dist/badge/badge.styles.ts +6 -1
  18. package/dist/badge/badge.ts +24 -2
  19. package/dist/button/button.styles.ts +10 -4
  20. package/dist/button/button.ts +4 -1
  21. package/dist/callout/callout.ts +12 -3
  22. package/dist/card/card.styles.ts +10 -1
  23. package/dist/card/card.ts +14 -0
  24. package/dist/card/index.ts +3 -0
  25. package/dist/diff-viewer/diff-viewer.styles.ts +3 -0
  26. package/dist/funnel-chart/funnel-chart.styles.ts +16 -0
  27. package/dist/funnel-chart/funnel-chart.ts +68 -0
  28. package/dist/funnel-chart/index.ts +10 -0
  29. package/dist/index.ts +4 -0
  30. package/dist/input/input.styles.ts +2 -2
  31. package/dist/line-chart/index.ts +19 -0
  32. package/dist/line-chart/line-chart.styles.ts +16 -0
  33. package/dist/line-chart/line-chart.ts +188 -0
  34. package/dist/page-header/index.ts +15 -0
  35. package/dist/page-header/page-header.styles.ts +21 -0
  36. package/dist/page-header/page-header.ts +27 -0
  37. package/dist/presence-indicator/presence-indicator.styles.ts +1 -1
  38. package/dist/progress-display/progress-display.styles.ts +1 -1
  39. package/dist/select/index.ts +2 -0
  40. package/dist/select/select.styles.ts +9 -3
  41. package/dist/select/select.ts +11 -3
  42. package/dist/separator/index.ts +2 -0
  43. package/dist/separator/separator.styles.ts +17 -2
  44. package/dist/shared/cn.ts +6 -3
  45. package/dist/slide-viewer/slide-viewer.styles.ts +2 -0
  46. package/dist/status-indicator/status-indicator.styles.ts +4 -4
  47. package/dist/sticky-note/sticky-note.styles.ts +2 -0
  48. package/dist/tabs/index.ts +1 -0
  49. package/dist/tabs/tabs.ts +29 -0
  50. package/dist/textarea/textarea.styles.ts +2 -2
  51. package/package.json +5 -1
@@ -0,0 +1,33 @@
1
+ import { cva } from '../shared/index.ts'
2
+
3
+ /** The list: rows separated by hairlines, no bullets. */
4
+ export const actionListClass = 'm-0 flex list-none flex-col divide-y divide-border-subtle p-0'
5
+
6
+ /** A row button: full width, whole row is the hit target. */
7
+ export const actionListItemVariants = cva({
8
+ base:
9
+ 'flex w-full items-center gap-4 bg-transparent text-left text-sm text-foreground transition-colors ' +
10
+ 'hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring ' +
11
+ 'disabled:pointer-events-none disabled:opacity-50',
12
+ variants: {
13
+ density: {
14
+ default: 'px-4 py-3',
15
+ compact: 'px-3 py-2',
16
+ },
17
+ },
18
+ defaultVariants: {
19
+ density: 'default',
20
+ },
21
+ })
22
+
23
+ /** Leading text column (title, description, meta). */
24
+ export const actionListItemBodyClass = 'flex min-w-0 flex-1 flex-col gap-0.5'
25
+ /** Primary line. */
26
+ export const actionListItemTitleClass = 'break-words font-semibold text-foreground'
27
+ /** Supporting line under the title. */
28
+ export const actionListItemDescriptionClass = 'break-words text-xs text-muted-foreground'
29
+ /** Tertiary line (owner, tags, ids). */
30
+ export const actionListItemMetaClass = 'break-words text-xs text-muted-foreground'
31
+ /** Trailing column (dates, sizes, counts). */
32
+ export const actionListItemTrailingClass =
33
+ 'flex flex-none items-center gap-4 text-xs tabular-nums text-muted-foreground'
@@ -0,0 +1,31 @@
1
+ /** Row padding. */
2
+ export type ActionListDensity = 'default' | 'compact'
3
+
4
+ export interface ActionListItemProps {
5
+ disabled?: boolean
6
+ density?: ActionListDensity
7
+ }
8
+
9
+ export interface ActionListItemAPI {
10
+ /** ARIA attributes for the row button. */
11
+ ariaProps: Record<string, string | number | boolean>
12
+ /** Data attributes for styling hooks. */
13
+ dataAttributes: Record<string, string>
14
+ }
15
+
16
+ /**
17
+ * Build the framework-agnostic props for one ActionList row. Each row is ONE
18
+ * action (open, select, go to) rendered as a native button, so it is reachable
19
+ * by Tab and activated by Enter / Space — unlike a clickable table row.
20
+ */
21
+ export function createActionListItem(props: ActionListItemProps = {}): ActionListItemAPI {
22
+ const { disabled = false, density = 'default' } = props
23
+ const dataAttributes: Record<string, string> = {
24
+ 'data-slot': 'action-list-item',
25
+ 'data-density': density,
26
+ }
27
+ if (disabled) dataAttributes['data-disabled'] = ''
28
+ const ariaProps: Record<string, string | number | boolean> = {}
29
+ if (disabled) ariaProps['aria-disabled'] = true
30
+ return { ariaProps, dataAttributes }
31
+ }
@@ -0,0 +1,16 @@
1
+ export {
2
+ createActionListItem,
3
+ type ActionListDensity,
4
+ type ActionListItemProps,
5
+ type ActionListItemAPI,
6
+ } from './action-list.js'
7
+
8
+ export {
9
+ actionListClass,
10
+ actionListItemVariants,
11
+ actionListItemBodyClass,
12
+ actionListItemTitleClass,
13
+ actionListItemDescriptionClass,
14
+ actionListItemMetaClass,
15
+ actionListItemTrailingClass,
16
+ } from './action-list.styles.js'
@@ -0,0 +1,12 @@
1
+ ---
2
+ import { actionListClass } from '../action-list/index.ts'
3
+ import { cn } from '../shared/index.ts'
4
+
5
+ interface Props extends astroHTML.JSX.HTMLAttributes {}
6
+
7
+ const { class: className, ...rest } = Astro.props
8
+ ---
9
+
10
+ <ul class={cn(actionListClass, className)} data-slot="action-list" {...rest}>
11
+ <slot />
12
+ </ul>
@@ -0,0 +1,62 @@
1
+ ---
2
+ import {
3
+ createActionListItem,
4
+ actionListItemVariants,
5
+ actionListItemBodyClass,
6
+ actionListItemTitleClass,
7
+ actionListItemDescriptionClass,
8
+ actionListItemMetaClass,
9
+ actionListItemTrailingClass,
10
+ type ActionListDensity,
11
+ } from '../action-list/index.ts'
12
+ import { cn } from '../shared/index.ts'
13
+
14
+ interface Props extends astroHTML.JSX.ButtonHTMLAttributes {
15
+ /** Primary line. */
16
+ title: string
17
+ /** Optional supporting line under the title. */
18
+ description?: string
19
+ /** Optional tertiary line. */
20
+ meta?: string
21
+ /** Optional trailing column (or use the `trailing` slot). */
22
+ trailing?: string
23
+ /** Row padding. */
24
+ density?: ActionListDensity
25
+ disabled?: boolean
26
+ }
27
+
28
+ const {
29
+ title,
30
+ description,
31
+ meta,
32
+ trailing,
33
+ density = 'default',
34
+ disabled = false,
35
+ class: className,
36
+ ...rest
37
+ } = Astro.props
38
+ const api = createActionListItem({ disabled, density })
39
+ const hasTrailing = trailing != null || Astro.slots.has('trailing')
40
+ ---
41
+
42
+ <li>
43
+ <button
44
+ type="button"
45
+ disabled={disabled}
46
+ class={cn(actionListItemVariants({ density }), className)}
47
+ {...api.ariaProps}
48
+ {...api.dataAttributes}
49
+ {...rest}
50
+ >
51
+ <span class={actionListItemBodyClass}>
52
+ <span class={actionListItemTitleClass}>{title}</span>
53
+ {description && <span class={actionListItemDescriptionClass}>{description}</span>}
54
+ {meta && <span class={actionListItemMetaClass}>{meta}</span>}
55
+ </span>
56
+ {hasTrailing && (
57
+ <span class={actionListItemTrailingClass}>
58
+ <slot name="trailing">{trailing}</slot>
59
+ </span>
60
+ )}
61
+ </button>
62
+ </li>
@@ -0,0 +1 @@
1
+ export {}
@@ -7,7 +7,10 @@ interface Props extends astroHTML.JSX.HTMLAttributes {
7
7
  }
8
8
 
9
9
  const { variant = 'default', class: className, ...props } = Astro.props
10
- const api = createCallout({ role: variant === 'destructive' ? 'alert' : 'region' })
10
+ const api = createCallout({
11
+ destructive: variant === 'destructive',
12
+ labelled: Boolean(props['aria-label'] || props['aria-labelledby']),
13
+ })
11
14
  ---
12
15
 
13
16
  <div
@@ -3,15 +3,16 @@ import { createCard, cardVariants } from '../card/index.ts'
3
3
  import { cn } from '../shared/index.ts'
4
4
 
5
5
  interface Props extends astroHTML.JSX.HTMLAttributes {
6
+ variant?: 'default' | 'subtle' | 'tertiary'
6
7
  padding?: 'none' | 'default' | 'compact'
7
8
  }
8
9
 
9
- const { padding, class: className, ...props } = Astro.props
10
+ const { variant, padding, class: className, ...props } = Astro.props
10
11
  const api = createCard(props)
11
12
  ---
12
13
 
13
14
  <div
14
- class={cn(cardVariants({ padding }), className)}
15
+ class={cn(cardVariants({ variant, padding }), className)}
15
16
  {...api.ariaProps}
16
17
  {...api.dataAttributes}
17
18
  {...props}
@@ -1,15 +1,17 @@
1
1
  ---
2
- import { createCardTitle, cardTitleVariants } from '../card/index.ts'
2
+ import { createCardTitle, cardTitleVariants, type CardTitleLevel } from '../card/index.ts'
3
3
  import { cn } from '../shared/index.ts'
4
4
 
5
5
  interface Props extends astroHTML.JSX.HTMLAttributes {
6
6
  size?: 'sm' | 'default' | 'lg'
7
+ /** Heading element. Defaults to `h3`. */
8
+ as?: CardTitleLevel
7
9
  }
8
10
 
9
- const { size, class: className, ...props } = Astro.props
11
+ const { size, as: Heading = 'h3', class: className, ...props } = Astro.props
10
12
  const api = createCardTitle()
11
13
  ---
12
14
 
13
- <h3 class={cn(cardTitleVariants({ size }), className)} {...api.dataAttributes} {...props}>
15
+ <Heading class={cn(cardTitleVariants({ size }), className)} {...api.dataAttributes} {...props}>
14
16
  <slot />
15
- </h3>
17
+ </Heading>
@@ -0,0 +1,64 @@
1
+ ---
2
+ import {
3
+ computeFunnel,
4
+ funnelChartClasses as c,
5
+ funnelChartDefaults,
6
+ DEFAULT_FUNNEL_MIN_BAR_PERCENT,
7
+ type FunnelChartStep,
8
+ } from '../funnel-chart/index.ts'
9
+ import { cn } from '../shared/index.ts'
10
+
11
+ interface Props extends astroHTML.JSX.HTMLAttributes {
12
+ /** The funnel steps, top first. */
13
+ steps: FunnelChartStep[]
14
+ formatValue?: (value: number) => string
15
+ formatPercent?: (percent: number) => string
16
+ minBarPercent?: number
17
+ shareLabel?: (share: string) => string
18
+ conversionLabel?: (percent: string) => string
19
+ droppedLabel?: (count: string) => string
20
+ }
21
+
22
+ const {
23
+ steps,
24
+ formatValue = funnelChartDefaults.formatValue,
25
+ formatPercent = funnelChartDefaults.formatPercent,
26
+ minBarPercent = DEFAULT_FUNNEL_MIN_BAR_PERCENT,
27
+ shareLabel = funnelChartDefaults.shareLabel,
28
+ conversionLabel = funnelChartDefaults.conversionLabel,
29
+ droppedLabel = funnelChartDefaults.droppedLabel,
30
+ class: className,
31
+ ...rest
32
+ } = Astro.props
33
+ const rows = computeFunnel(steps, minBarPercent)
34
+ ---
35
+
36
+ <ol class={cn(c.root, className)} data-slot="funnel-chart" {...rest}>
37
+ {
38
+ rows.map((row) => (
39
+ <li>
40
+ <div class={c.bar}>
41
+ <div class={c.fill} style={`width:${row.barPercent}%`} aria-hidden="true" />
42
+ <svg class={c.chevron} width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
43
+ <path d="m9 18 6-6-6-6" />
44
+ </svg>
45
+ <div class={c.text}>
46
+ <b class={c.label}>{row.step.label}</b>
47
+ {row.step.description && <span class={c.description}>{row.step.description}</span>}
48
+ </div>
49
+ <div class={c.figure}>
50
+ <b class={c.value}>{formatValue(row.step.value)}</b>
51
+ <span class={c.share}>{shareLabel(formatPercent(row.shareOfTop))}</span>
52
+ </div>
53
+ </div>
54
+ {row.next && (
55
+ <div class={c.transition}>
56
+ <span class={c.conversion}>{conversionLabel(formatPercent(row.next.conversion))}</span>
57
+ <span aria-hidden="true">·</span>
58
+ <span class={c.dropped}>{droppedLabel(formatValue(row.next.dropped))}</span>
59
+ </div>
60
+ )}
61
+ </li>
62
+ ))
63
+ }
64
+ </ol>
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,82 @@
1
+ ---
2
+ import {
3
+ computeLineChart,
4
+ lineChartClasses as c,
5
+ DEFAULT_LINE_CHART_WIDTH,
6
+ DEFAULT_LINE_CHART_HEIGHT,
7
+ type LineChartSeries,
8
+ } from '../line-chart/index.ts'
9
+ import { cn } from '../shared/index.ts'
10
+
11
+ interface Props extends astroHTML.JSX.HTMLAttributes {
12
+ /** The series to plot, all on one shared y-scale. */
13
+ series: LineChartSeries[]
14
+ /** One category label per data point. */
15
+ labels: string[]
16
+ /** Accessible name of the chart image. */
17
+ ariaLabel: string
18
+ height?: number
19
+ width?: number
20
+ /** Formats a y-axis tick. */
21
+ formatTick?: (value: number) => string
22
+ /** Fill the area under each line. Defaults to on for a single series only. */
23
+ area?: boolean
24
+ showLegend?: boolean
25
+ }
26
+
27
+ const {
28
+ series,
29
+ labels,
30
+ ariaLabel,
31
+ height = DEFAULT_LINE_CHART_HEIGHT,
32
+ width = DEFAULT_LINE_CHART_WIDTH,
33
+ formatTick = (value: number) => String(Math.round(value)),
34
+ area,
35
+ showLegend = true,
36
+ class: className,
37
+ ...rest
38
+ } = Astro.props
39
+ const geo = computeLineChart({ series, labels, width, height })
40
+ const fillArea = area ?? series.length === 1
41
+ const gradientBase = `line-chart-${ariaLabel.replace(/[^a-zA-Z0-9]+/g, '-').toLowerCase()}`
42
+ ---
43
+
44
+ <div class={cn(c.root, className)} data-slot="line-chart" {...rest}>
45
+ <svg class={c.svg} viewBox={`0 0 ${width} ${height}`} style={`height:${height}px`} role="img" aria-label={ariaLabel}>
46
+ {geo.ticks.map((t) => (
47
+ <g>
48
+ <line class={c.gridline} x1={geo.padding.left} y1={t.y} x2={width - geo.padding.right} y2={t.y} />
49
+ <text class={c.axis} x={geo.padding.left - 8} y={t.y + 3} text-anchor="end">{formatTick(t.value)}</text>
50
+ </g>
51
+ ))}
52
+ {geo.xLabels.map((l) => (
53
+ <text class={c.axis} x={l.x} y={height - 6} text-anchor="middle">{l.label}</text>
54
+ ))}
55
+ {geo.paths.map((p, si) => (
56
+ <g>
57
+ {fillArea && (
58
+ <Fragment>
59
+ <defs>
60
+ <linearGradient id={`${gradientBase}-${si}`} x1="0" y1="0" x2="0" y2="1">
61
+ <stop offset="0%" stop-color={p.color} stop-opacity="0.22" />
62
+ <stop offset="100%" stop-color={p.color} stop-opacity="0" />
63
+ </linearGradient>
64
+ </defs>
65
+ <path d={p.area} fill={`url(#${gradientBase}-${si})`} />
66
+ </Fragment>
67
+ )}
68
+ <path d={p.line} fill="none" stroke={p.color} stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" />
69
+ </g>
70
+ ))}
71
+ </svg>
72
+ {showLegend && (
73
+ <div class={c.legend}>
74
+ {series.map((s, si) => (
75
+ <span class={c.legendItem}>
76
+ <i class={c.legendSwatch} style={`background:${geo.paths[si].color}`} aria-hidden="true" />
77
+ {s.name}
78
+ </span>
79
+ ))}
80
+ </div>
81
+ )}
82
+ </div>
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,46 @@
1
+ ---
2
+ import {
3
+ createPageHeader,
4
+ pageHeaderClass,
5
+ pageHeaderTextClass,
6
+ pageHeaderKickerClass,
7
+ pageHeaderTitleClass,
8
+ pageHeaderDescriptionClass,
9
+ pageHeaderActionsClass,
10
+ type PageHeaderTitleLevel,
11
+ } from '../page-header/index.ts'
12
+ import { cn } from '../shared/index.ts'
13
+
14
+ interface Props extends astroHTML.JSX.HTMLAttributes {
15
+ /** Optional eyebrow rendered above the title. */
16
+ kicker?: string
17
+ /** The page title. */
18
+ title: string
19
+ /** Optional supporting copy under the title (or use the `description` slot). */
20
+ description?: string
21
+ /** Heading element for the title. Defaults to `h1`. */
22
+ as?: PageHeaderTitleLevel
23
+ }
24
+
25
+ const { kicker, title, description, as: Title = 'h1', class: className, ...rest } = Astro.props
26
+ const hasActions = Astro.slots.has('actions')
27
+ const hasDescription = description != null || Astro.slots.has('description')
28
+ const { dataAttributes } = createPageHeader({ hasActions })
29
+ ---
30
+
31
+ <header class={cn(pageHeaderClass, className)} {...dataAttributes} {...rest}>
32
+ <div class={pageHeaderTextClass}>
33
+ {kicker && <p class={pageHeaderKickerClass}>{kicker}</p>}
34
+ <Title class={pageHeaderTitleClass}>{title}</Title>
35
+ {hasDescription && (
36
+ <div class={pageHeaderDescriptionClass}>
37
+ <slot name="description">{description}</slot>
38
+ </div>
39
+ )}
40
+ </div>
41
+ {hasActions && (
42
+ <div class={pageHeaderActionsClass}>
43
+ <slot name="actions" />
44
+ </div>
45
+ )}
46
+ </header>
@@ -0,0 +1 @@
1
+ export {}
@@ -50,7 +50,7 @@ export const avatarPresenceDotVariants = cva({
50
50
  },
51
51
  status: {
52
52
  online: 'bg-success',
53
- offline: 'bg-gray-400',
53
+ offline: 'bg-neutral',
54
54
  away: 'bg-warning',
55
55
  busy: 'bg-destructive',
56
56
  dnd: 'bg-destructive',
@@ -1,7 +1,7 @@
1
1
  import { cva } from '../shared/index.ts'
2
2
 
3
3
  export const badgeVariants = cva({
4
- base: 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
4
+ base: 'inline-flex items-center rounded-full border font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
5
5
  variants: {
6
6
  variant: {
7
7
  default: 'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
@@ -11,6 +11,11 @@ export const badgeVariants = cva({
11
11
  outline: 'text-foreground',
12
12
  success: 'border-transparent bg-success text-success-foreground shadow hover:bg-success/80 font-semibold',
13
13
  warning: 'border-transparent bg-warning text-warning-foreground shadow hover:bg-warning/80 font-semibold',
14
+ // Second brand accent + extended status roles (issue #485).
15
+ tertiary: 'border-transparent bg-tertiary text-tertiary-foreground shadow hover:bg-tertiary/80 font-semibold',
16
+ positive: 'border-transparent bg-positive text-positive-foreground shadow hover:bg-positive/80 font-semibold',
17
+ caution: 'border-transparent bg-caution text-caution-foreground shadow hover:bg-caution/80 font-semibold',
18
+ done: 'border-transparent bg-done text-info-foreground shadow hover:bg-done/80 font-semibold',
14
19
  },
15
20
  size: {
16
21
  sm: 'px-2 py-0 text-[10px]',
@@ -1,6 +1,18 @@
1
1
  import type { AccessibilityProps } from '../shared/index.ts'
2
2
 
3
- export type BadgeVariant = 'default' | 'primary' | 'secondary' | 'destructive' | 'outline' | 'success' | 'warning'
3
+ export type BadgeVariant =
4
+ | 'default'
5
+ | 'primary'
6
+ | 'secondary'
7
+ | 'destructive'
8
+ | 'outline'
9
+ | 'success'
10
+ | 'warning'
11
+ // Issue #485 — second brand accent + extended status roles.
12
+ | 'tertiary'
13
+ | 'positive'
14
+ | 'caution'
15
+ | 'done'
4
16
  export type BadgeSize = 'sm' | 'md'
5
17
 
6
18
  export interface BadgeProps {
@@ -15,13 +27,23 @@ export interface BadgeAPI {
15
27
  dataAttributes: Record<string, string>
16
28
  }
17
29
 
18
- const statusVariants: BadgeVariant[] = ['success', 'warning', 'destructive']
30
+ const statusVariants: BadgeVariant[] = [
31
+ 'success',
32
+ 'warning',
33
+ 'destructive',
34
+ 'positive',
35
+ 'caution',
36
+ 'done',
37
+ ]
19
38
 
20
39
  /** Icon name for status badge variants — non-color indicator for colorblind safety */
21
40
  const BADGE_ICONS: Partial<Record<BadgeVariant, string>> = {
22
41
  success: 'check',
23
42
  warning: 'alert',
24
43
  destructive: 'x',
44
+ positive: 'check',
45
+ caution: 'alert',
46
+ done: 'check',
25
47
  }
26
48
 
27
49
  /** Get the icon name for a badge variant (undefined for non-status variants) */
@@ -13,7 +13,7 @@ export const buttonTokens: TokenContract = {
13
13
  }
14
14
 
15
15
  export const buttonVariants = cva({
16
- base: 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0',
16
+ base: 'inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0',
17
17
  variants: {
18
18
  variant: {
19
19
  default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
@@ -24,15 +24,21 @@ export const buttonVariants = cva({
24
24
  destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
25
25
  outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
26
26
  secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
27
+ // Soft tinted brand fill — lower-emphasis primary action (issue #485).
28
+ soft: 'bg-primary-soft text-primary-soft-foreground shadow-sm hover:bg-primary-soft/80',
29
+ // Second brand accent (issue #485).
30
+ tertiary: 'bg-tertiary text-tertiary-foreground shadow-sm hover:bg-tertiary/90',
27
31
  ghost: 'hover:bg-accent hover:text-accent-foreground',
28
32
  link: 'text-primary underline-offset-4 hover:underline',
29
33
  },
30
34
  size: {
31
35
  xs: 'h-7 rounded px-2 text-xs',
32
36
  sm: 'h-8 rounded-md px-3 text-xs',
33
- default: 'h-9 px-4 py-2',
34
- lg: 'h-10 rounded-md px-8',
35
- icon: 'h-9 w-9',
37
+ default: 'h-9 rounded-md px-4 py-2 text-sm',
38
+ lg: 'h-10 rounded-md px-8 text-sm',
39
+ icon: 'h-9 w-9 rounded-md text-sm',
40
+ 'icon-sm': 'h-8 w-8 rounded-md text-xs',
41
+ 'icon-xs': 'h-7 w-7 rounded text-xs',
36
42
  },
37
43
  },
38
44
  defaultVariants: {
@@ -14,6 +14,9 @@ export type ButtonVariant =
14
14
  | 'secondary'
15
15
  | 'ghost'
16
16
  | 'link'
17
+ // Issue #485 — soft tinted brand fill + a real second brand accent.
18
+ | 'soft'
19
+ | 'tertiary'
17
20
 
18
21
  /** Variants that map to the same visual style as `default`. */
19
22
  export const BUTTON_VARIANT_ALIASES: Partial<Record<ButtonVariant, ButtonVariant>> = {
@@ -25,7 +28,7 @@ export function resolveButtonVariant(variant: ButtonVariant | undefined): Button
25
28
  if (!variant) return variant
26
29
  return BUTTON_VARIANT_ALIASES[variant] ?? variant
27
30
  }
28
- export type ButtonSize = 'xs' | 'sm' | 'default' | 'lg' | 'icon'
31
+ export type ButtonSize = 'xs' | 'sm' | 'default' | 'lg' | 'icon' | 'icon-sm' | 'icon-xs'
29
32
 
30
33
  export interface ButtonProps {
31
34
  variant?: ButtonVariant
@@ -1,8 +1,16 @@
1
1
  import type { AccessibilityProps } from '../shared/index.ts'
2
2
 
3
3
  export interface CalloutProps {
4
- /** Optional accessible role override */
4
+ /** Explicit role; always wins. */
5
5
  role?: string
6
+ /** Destructive callouts are announced (`role="alert"`). */
7
+ destructive?: boolean
8
+ /**
9
+ * Whether the callout has an accessible name (`aria-label` /
10
+ * `aria-labelledby`). Only a named callout becomes a `region` landmark — an
11
+ * unnamed region is an axe violation and landmark noise.
12
+ */
13
+ labelled?: boolean
6
14
  }
7
15
 
8
16
  export interface CalloutAPI {
@@ -13,10 +21,11 @@ export interface CalloutAPI {
13
21
  export function createCallout(props: CalloutProps = {}): CalloutAPI {
14
22
  const ariaProps: Partial<AccessibilityProps> = {}
15
23
 
16
- // Callouts are usually alert or region depending on severity
17
24
  if (props.role) {
18
25
  ariaProps.role = props.role
19
- } else {
26
+ } else if (props.destructive) {
27
+ ariaProps.role = 'alert'
28
+ } else if (props.labelled) {
20
29
  ariaProps.role = 'region'
21
30
  }
22
31
 
@@ -11,8 +11,16 @@ export const cardTokens: TokenContract = {
11
11
  }
12
12
 
13
13
  export const cardVariants = cva({
14
- base: 'rounded-lg border bg-card text-card-foreground shadow',
14
+ base: 'rounded-lg border shadow',
15
15
  variants: {
16
+ variant: {
17
+ // Default — unchanged surface (bg-card + border via the global token).
18
+ default: 'bg-card text-card-foreground',
19
+ // Lower-emphasis container on the subtle surface token (issue #485).
20
+ subtle: 'bg-surface-subtle text-card-foreground',
21
+ // Tinted second-accent card (issue #485).
22
+ tertiary: 'border-transparent bg-tertiary-soft text-tertiary-soft-foreground',
23
+ },
16
24
  padding: {
17
25
  none: '',
18
26
  default: 'p-6',
@@ -20,6 +28,7 @@ export const cardVariants = cva({
20
28
  },
21
29
  },
22
30
  defaultVariants: {
31
+ variant: 'default',
23
32
  padding: 'none',
24
33
  },
25
34
  })
package/dist/card/card.ts CHANGED
@@ -1,5 +1,19 @@
1
1
  import type { AccessibilityProps } from '../shared/index.ts'
2
2
 
3
+ /**
4
+ * Visual tone of a card (issue #485).
5
+ * - `default` — the standard surface (activates the `--rfr-card-*` contract).
6
+ * - `subtle` — a lower-emphasis container on the subtle surface token.
7
+ * - `tertiary` — a tinted second-brand-accent card.
8
+ */
9
+ export type CardVariant = 'default' | 'subtle' | 'tertiary'
10
+
11
+ /** Built-in inner padding of the Card container. */
12
+ export type CardPadding = 'none' | 'default' | 'compact'
13
+
14
+ /** Heading element for CardTitle. */
15
+ export type CardTitleLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
16
+
3
17
  export interface CardProps {
4
18
  /** Optional accessible role override */
5
19
  role?: string
@@ -7,6 +7,9 @@ export {
7
7
  createCardFooter,
8
8
  type CardProps,
9
9
  type CardAPI,
10
+ type CardVariant,
11
+ type CardPadding,
12
+ type CardTitleLevel,
10
13
  } from './card.js'
11
14
 
12
15
  export {
@@ -9,6 +9,9 @@ export const diffViewerTokens: TokenContract = {
9
9
  sidebarBg: { variable: '--rfr-diff-sidebar-bg', fallback: 'hsl(var(--muted))' },
10
10
  headerBg: { variable: '--rfr-diff-header-bg', fallback: 'hsl(var(--muted))' },
11
11
  border: { variable: '--rfr-diff-border', fallback: 'hsl(var(--border))' },
12
+ // Diff add/del colors are syntax semantics (like code highlighting), not
13
+ // theme status tones — deliberately NOT routed through the success/
14
+ // destructive tokens. The --rfr-diff-* variables are the theming seam.
12
15
  addBg: { variable: '--rfr-diff-add-bg', fallback: 'rgba(46, 160, 67, 0.15)' },
13
16
  delBg: { variable: '--rfr-diff-del-bg', fallback: 'rgba(248, 81, 73, 0.15)' },
14
17
  addFg: { variable: '--rfr-diff-add-fg', fallback: '#3fb950' },