@scalepad/ui 0.1.0 → 0.1.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/README.md CHANGED
@@ -25,7 +25,7 @@ pnpm add @mantine/core@9.0.1 @mantine/dates@9.0.1 @mantine/hooks@9.0.1 \
25
25
  @tiptap/extension-link@3.22.3 @tiptap/extension-placeholder@3.22.3 \
26
26
  @tiptap/extension-underline@3.22.3 @tiptap/pm@3.22.3 @tiptap/react@3.22.3 \
27
27
  @tiptap/starter-kit@3.22.3 @tiptap/suggestion@3.22.3 \
28
- @vanilla-extract/css@^1.16.2 clsx@^2.1.1 dayjs@^1.11.19 geist@^1.5.1 \
28
+ @vanilla-extract/css@^1.16.2 clsx@^2.1.1 dayjs@^1.11.19 \
29
29
  lucide-react@^0.469.0 react@^19.0.0 react-dom@^19.0.0 \
30
30
  react-intersection-observer@^10.0.0 recharts@^3.6.0
31
31
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalepad/ui",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "ScalePad LM Design System — React + Mantine 9 + vanilla-extract component library",
@@ -58,7 +58,6 @@
58
58
  "@vanilla-extract/css": "^1.16.2",
59
59
  "clsx": "^2.1.1",
60
60
  "dayjs": "^1.11.19",
61
- "geist": "^1.5.1",
62
61
  "lucide-react": "^0.469.0",
63
62
  "react": "^19.0.0",
64
63
  "react-dom": "^19.0.0",
@@ -102,7 +101,6 @@
102
101
  "@vitest/coverage-v8": "^4.0.17",
103
102
  "clsx": "^2.1.1",
104
103
  "dayjs": "^1.11.19",
105
- "geist": "^1.5.1",
106
104
  "lucide-react": "^0.469.0",
107
105
  "playwright": "^1.57.0",
108
106
  "prop-types": "^15.8.1",
@@ -7,7 +7,7 @@ import '@mantine/notifications/styles.css';
7
7
  // oxlint-disable-next-line import/no-unassigned-import
8
8
  import '@mantine/schedule/styles.css';
9
9
  // oxlint-disable-next-line import/no-unassigned-import
10
- import './assets/geist-fonts.css';
10
+ import './inter-font';
11
11
 
12
12
  import type { ReactNode } from 'react';
13
13
 
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Anchor component styles – vanilla-extract with semantic design tokens.
3
+ *
4
+ * Anchor is a polymorphic component that renders as `<a>` (default) or
5
+ * `<button>`. It covers two related use cases with the same visual model:
6
+ * inline links inside body copy, and text-only "buttons" (no fill, no
7
+ * border) like the mockup's "Generate" / "Regenerate".
8
+ *
9
+ * Typography is driven entirely by `variant` (BodyVariant) — one class per
10
+ * variant generated from `textStyleVariants`. There is no inline `fz`/`fw`
11
+ * forwarding; the variant class is the only path that sets typography.
12
+ */
13
+
14
+ import { style, styleVariants } from '@vanilla-extract/css';
15
+
16
+ import { textStyleVariants, type BodyVariant } from '../../tokens/text-styles';
17
+ import { mantineVars } from '../../theme/mantineVars';
18
+ import { tokens } from '../../theme/themeContract.css';
19
+
20
+ const focusRing = {
21
+ outline: `2px solid ${tokens.color.stroke.focusStrong}`,
22
+ outlineOffset: 2,
23
+ borderRadius: tokens.radius.sm,
24
+ } as const;
25
+
26
+ /**
27
+ * Base styles applied to every Anchor: clear underline at rest (the design
28
+ * system's links underline only on interaction), reset background/border so
29
+ * `component="button"` looks identical to `<a>`, and apply the focus ring
30
+ * + hover underline.
31
+ */
32
+ export const root = style({
33
+ backgroundColor: 'transparent',
34
+ border: 'none',
35
+ padding: 0,
36
+ margin: 0,
37
+ cursor: 'pointer',
38
+ textDecoration: 'none',
39
+ textDecorationThickness: '1px',
40
+ textUnderlineOffset: '2px',
41
+ transition: 'color 150ms ease, text-decoration-color 150ms ease',
42
+ selectors: {
43
+ '&:hover': {
44
+ textDecoration: 'underline',
45
+ },
46
+ '&:focus-visible': focusRing,
47
+ },
48
+ });
49
+
50
+ /**
51
+ * One class per BodyVariant — typography is generated from the same
52
+ * `textStyleVariants` source that `Text` and `Title` use, so they cannot
53
+ * drift.
54
+ */
55
+ export const variant = styleVariants(textStyleVariants, styles => ({
56
+ fontFamily: styles.fontFamily,
57
+ fontWeight: styles.fontWeight,
58
+ fontSize: styles.fontSize,
59
+ lineHeight: styles.lineHeight,
60
+ letterSpacing: styles.letterSpacing,
61
+ textTransform: 'textTransform' in styles ? styles.textTransform : undefined,
62
+ })) satisfies Record<BodyVariant, string>;
63
+
64
+ /**
65
+ * One class per AnchorTone. Color at rest + hover. The `:hover` underline
66
+ * comes from `root`; tone classes only set `color`. Dark-mode overrides
67
+ * pin to the same semantic tokens (which already swap per theme via the
68
+ * theme contract).
69
+ */
70
+ export const tone = styleVariants({
71
+ default: {
72
+ color: tokens.color.text.default,
73
+ selectors: {
74
+ '&:hover': { color: tokens.color.text.primaryDefault },
75
+ '&:focus-visible': { color: tokens.color.text.primaryDefault },
76
+ [`${mantineVars.darkSelector} &`]: { color: tokens.color.text.default },
77
+ },
78
+ },
79
+ primary: {
80
+ color: tokens.color.text.primaryDefault,
81
+ selectors: {
82
+ '&:hover': { color: tokens.color.text.primaryLight },
83
+ '&:focus-visible': { color: tokens.color.text.primaryLight },
84
+ [`${mantineVars.darkSelector} &`]: {
85
+ color: tokens.color.text.primaryDefault,
86
+ },
87
+ },
88
+ },
89
+ danger: {
90
+ color: tokens.color.text.dangerDefault,
91
+ selectors: {
92
+ '&:hover': { color: tokens.color.text.dangerStrong },
93
+ '&:focus-visible': { color: tokens.color.text.dangerStrong },
94
+ [`${mantineVars.darkSelector} &`]: {
95
+ color: tokens.color.text.dangerDefault,
96
+ },
97
+ },
98
+ },
99
+ subdued: {
100
+ color: tokens.color.text.subduedStrong,
101
+ selectors: {
102
+ '&:hover': { color: tokens.color.text.default },
103
+ '&:focus-visible': { color: tokens.color.text.default },
104
+ [`${mantineVars.darkSelector} &`]: {
105
+ color: tokens.color.text.subduedStrong,
106
+ },
107
+ },
108
+ },
109
+ });
110
+
111
+ /**
112
+ * Layout for the case where `leftSection` and/or `rightSection` is set.
113
+ * Without icons we leave the root as plain inline text so it flows
114
+ * naturally inside a paragraph.
115
+ */
116
+ export const withIcons = style({
117
+ display: 'inline-flex',
118
+ alignItems: 'center',
119
+ verticalAlign: 'baseline',
120
+ });
121
+
122
+ /** Gap between icon section(s) and label, sized to match Figma `Anchor`. */
123
+ export const gap = styleVariants({
124
+ xs: { gap: 4 },
125
+ sm: { gap: 4 },
126
+ md: { gap: 6 },
127
+ lg: { gap: 6 },
128
+ });
129
+
130
+ /**
131
+ * Wrapper around left / right icon nodes. `display: inline-flex` keeps the
132
+ * icon visually centered with the label without forcing the caller to wrap
133
+ * their lucide icon themselves.
134
+ */
135
+ export const iconSection = style({
136
+ display: 'inline-flex',
137
+ alignItems: 'center',
138
+ flexShrink: 0,
139
+ });
140
+
141
+ /**
142
+ * Disabled state — used by both `<a aria-disabled>` and `<button disabled>`.
143
+ * `pointer-events: none` neutralises hover/click without removing focus
144
+ * ability when needed (e.g. screenreader can still navigate to the
145
+ * `aria-disabled` link).
146
+ */
147
+ export const disabled = style({
148
+ color: tokens.color.text.disabledDefault,
149
+ cursor: 'not-allowed',
150
+ pointerEvents: 'none',
151
+ selectors: {
152
+ '&:hover': {
153
+ textDecoration: 'none',
154
+ color: tokens.color.text.disabledDefault,
155
+ },
156
+ '&:focus-visible': {
157
+ color: tokens.color.text.disabledDefault,
158
+ },
159
+ [`${mantineVars.darkSelector} &`]: {
160
+ color: tokens.color.text.disabledDefault,
161
+ },
162
+ },
163
+ });
@@ -0,0 +1,57 @@
1
+ import figma from '@figma/code-connect';
2
+
3
+ import { Anchor } from './Anchor';
4
+
5
+ /**
6
+ * Code Connect mapping for the LM Design System `Anchor` component_set
7
+ * (formerly `Link Button`, renamed to keep design + code names aligned).
8
+ *
9
+ * The component_set lives on page `Anchor` (node `842:44446`) in
10
+ * `LM Design System` (`VCLfybgU3OaUUPrQdBaVmP`). Variants exposed:
11
+ *
12
+ * - `Variant`: Default | Primary | Danger | Subdued — color tone.
13
+ * - `Size`: Mini | Small | Default | Large — icon-gap scale.
14
+ * - `Roundness`, `State` (Default | Hover & Active | Focus | Disabled):
15
+ * not surfaced as code props. Hover / focus / disabled are interaction
16
+ * state; `Roundness` only changes the focus-ring shape.
17
+ * - Boolean-gated icon swaps `Show left icon` + `⮑ Left icon` and
18
+ * `Show right icon` + `⮑ Right icon` map to `leftSection` / `rightSection`.
19
+ */
20
+ figma.connect(
21
+ Anchor,
22
+ 'https://www.figma.com/design/VCLfybgU3OaUUPrQdBaVmP/LM-Design-System?node-id=842-44446',
23
+ {
24
+ props: {
25
+ tone: figma.enum('Variant', {
26
+ Default: 'default',
27
+ Primary: 'primary',
28
+ Danger: 'danger',
29
+ Subdued: 'subdued',
30
+ }),
31
+ size: figma.enum('Size', {
32
+ Default: 'md',
33
+ Large: 'lg',
34
+ Small: 'sm',
35
+ Mini: 'xs',
36
+ }),
37
+ leftSection: figma.boolean('Show left icon', {
38
+ true: figma.instance('⮑ Left icon'),
39
+ false: undefined,
40
+ }),
41
+ rightSection: figma.boolean('Show right icon', {
42
+ true: figma.instance('⮑ Right icon'),
43
+ false: undefined,
44
+ }),
45
+ },
46
+ example: props => (
47
+ <Anchor
48
+ tone={props.tone}
49
+ size={props.size}
50
+ leftSection={props.leftSection}
51
+ rightSection={props.rightSection}
52
+ >
53
+ Label
54
+ </Anchor>
55
+ ),
56
+ },
57
+ );
@@ -1,3 +1,33 @@
1
+ /**
2
+ * Design System Anchor Component
3
+ *
4
+ * Polymorphic — renders as `<a>` by default and `component="button"` for
5
+ * action triggers (e.g. inline "Generate" / "Regenerate" actions). Covers
6
+ * both inline links and text-only "buttons" with one component.
7
+ *
8
+ * Typography is locked to the design-system body variants (no `fz`/`fw`/
9
+ * `lh`/etc. forwarded). Color is controlled via `tone`; use `c` only for
10
+ * one-off escapes (e.g. `c="inherit"` to flow color from parent).
11
+ *
12
+ * @example Inline link
13
+ * ```tsx
14
+ * <Anchor href="/clients/123">Open client</Anchor>
15
+ * ```
16
+ *
17
+ * @example Text-only button (mockup pattern)
18
+ * ```tsx
19
+ * <Anchor
20
+ * component="button"
21
+ * type="button"
22
+ * tone="primary"
23
+ * leftSection={<RefreshCcw size={14} />}
24
+ * onClick={onRegenerate}
25
+ * >
26
+ * Regenerate link
27
+ * </Anchor>
28
+ * ```
29
+ */
30
+
1
31
  import { forwardRef, type ReactNode } from 'react';
2
32
 
3
33
  import {
@@ -5,39 +35,110 @@ import {
5
35
  createPolymorphicComponent,
6
36
  type AnchorProps as MantineAnchorProps,
7
37
  } from '@mantine/core';
38
+ import { clsx } from 'clsx';
8
39
 
9
- import { textStyleVariants, type BodyVariant } from '../../tokens/text-styles';
10
40
  import { resolveColorToken } from '../../utils/color-props';
41
+ import type { TypographyStyleProp } from '../../utils/typography-props';
42
+
43
+ import * as classes from './Anchor.css';
44
+
45
+ import type { BodyVariant, TextColor } from '../../tokens';
11
46
 
12
- import type { TextColor } from '../../tokens/color-types';
47
+ export type AnchorTone = 'default' | 'primary' | 'danger' | 'subdued';
13
48
 
14
- type TypographyStyleProps = 'fw' | 'fz' | 'size' | 'lh' | 'ff' | 'lts' | 'fs';
49
+ /**
50
+ * Box size — controls the icon-to-label gap. Typography is independent and
51
+ * comes from `variant`. Maps to Figma `Anchor`'s `Size` property
52
+ * (Mini/Small/Default/Large).
53
+ */
54
+ export type AnchorSize = 'xs' | 'sm' | 'md' | 'lg';
15
55
 
16
56
  export type AnchorProps = Omit<
17
57
  MantineAnchorProps,
18
- TypographyStyleProps | 'c' | 'children'
58
+ TypographyStyleProp | 'c' | 'children' | 'variant' | 'color'
19
59
  > & {
60
+ /** Typography variant from the design system. Default `body1`. */
20
61
  variant?: BodyVariant;
62
+ /**
63
+ * Color tone. Maps to the Figma `Anchor` component's `Variant` property.
64
+ * Default `primary` (matches Mantine's link-green default and existing
65
+ * `<Anchor component="button">` usage).
66
+ */
67
+ tone?: AnchorTone;
68
+ /** Box size — controls icon gap. Default `md`. */
69
+ size?: AnchorSize;
70
+ /** Optional icon node rendered before the label. */
71
+ leftSection?: ReactNode;
72
+ /** Optional icon node rendered after the label. */
73
+ rightSection?: ReactNode;
74
+ /**
75
+ * Disabled state. For `<a>` renders `aria-disabled` and removes hover;
76
+ * for `<button>` adds the native `disabled` attribute.
77
+ */
78
+ disabled?: boolean;
79
+ /**
80
+ * Color override. Use sparingly — `tone` should cover semantic cases.
81
+ * `inherit` flows the color from the parent (e.g. inside a tinted `<Text>`).
82
+ */
21
83
  c?: TextColor | 'inherit';
22
84
  children?: ReactNode;
23
85
  };
24
86
 
25
87
  const AnchorBase = forwardRef<HTMLAnchorElement, AnchorProps>(
26
- ({ variant = 'body1', c, ...rest }, ref) => {
27
- const variantStyles = textStyleVariants[variant];
28
- const resolvedC = resolveColorToken(c);
88
+ (
89
+ {
90
+ variant = 'body1',
91
+ tone = 'primary',
92
+ size = 'md',
93
+ leftSection,
94
+ rightSection,
95
+ disabled,
96
+ className,
97
+ children,
98
+ c,
99
+ ...rest
100
+ },
101
+ ref,
102
+ ) => {
103
+ const hasIcons = leftSection != null || rightSection != null;
104
+ // Mantine forwards arbitrary `component` via polymorphic typing.
105
+ // Narrow at runtime so we can apply the right disabled semantics
106
+ // (native `disabled` for `<button>`, `aria-disabled` for `<a>`).
107
+ const isButton = (rest as { component?: unknown }).component === 'button';
108
+
109
+ const cls = clsx(
110
+ classes.root,
111
+ classes.variant[variant],
112
+ classes.tone[tone],
113
+ hasIcons && classes.withIcons,
114
+ hasIcons && classes.gap[size],
115
+ disabled && classes.disabled,
116
+ className,
117
+ );
118
+
119
+ const resolvedC = c !== undefined ? resolveColorToken(c) : undefined;
29
120
 
30
121
  return (
31
122
  <MantineAnchor
32
123
  ref={ref}
33
- fw={variantStyles.fontWeight}
34
- fz={variantStyles.fontSize}
35
- lh={variantStyles.lineHeight}
36
- lts={variantStyles.letterSpacing}
37
- ff={variantStyles.fontFamily}
124
+ className={cls}
38
125
  c={resolvedC}
126
+ aria-disabled={!isButton && disabled ? true : undefined}
127
+ {...(isButton && disabled ? { disabled: true } : {})}
39
128
  {...rest}
40
- />
129
+ >
130
+ {leftSection != null && (
131
+ <span className={classes.iconSection} aria-hidden>
132
+ {leftSection}
133
+ </span>
134
+ )}
135
+ {children}
136
+ {rightSection != null && (
137
+ <span className={classes.iconSection} aria-hidden>
138
+ {rightSection}
139
+ </span>
140
+ )}
141
+ </MantineAnchor>
41
142
  );
42
143
  },
43
144
  );
@@ -1,2 +1,2 @@
1
1
  export { Anchor } from './Anchor';
2
- export type { AnchorProps } from './Anchor';
2
+ export type { AnchorProps, AnchorSize, AnchorTone } from './Anchor';
@@ -193,12 +193,16 @@ export function SearchableFilterSubmenu({
193
193
  ) : displayedItems.length > 0 ? (
194
194
  <>
195
195
  {displayedItems.map(item => (
196
+ // Toggling flows through the Checkbox's native onChange (input
197
+ // click + label click both delegate there). We deliberately
198
+ // don't put `onClick` on Menu.Item: Mantine's Checkbox renders
199
+ // its label as a `<label for=input>` whose click event keeps
200
+ // bubbling after delegating to the input, so a Menu.Item.onClick
201
+ // would catch the bubbled event and fire a second toggle that
202
+ // net-cancels the first. `onKeyDown` for space stays so the
203
+ // row can still be toggled when focused via keyboard.
196
204
  <Menu.Item
197
205
  key={item.id}
198
- onClick={e => {
199
- e.stopPropagation();
200
- toggleItem(item.id);
201
- }}
202
206
  closeMenuOnClick={false}
203
207
  onKeyDown={e => {
204
208
  if (e.key === ' ') {
@@ -212,7 +216,6 @@ export function SearchableFilterSubmenu({
212
216
  <Checkbox
213
217
  checked={selectedIds.has(item.id)}
214
218
  onChange={() => toggleItem(item.id)}
215
- onClick={e => e.stopPropagation()}
216
219
  size="sm"
217
220
  label={item.name}
218
221
  />
@@ -14,8 +14,11 @@ export function extractFilterValue(
14
14
  );
15
15
 
16
16
  if (schema.type === 'boolean') {
17
- // Boolean filters: true if category exists and has items, false otherwise
18
- return category ? category.items.length > 0 : false;
17
+ // Boolean filters are binary they have no concept of items. The
18
+ // canonical writer (`createFilterCategory`) emits a stub `{id, name:'True'}`
19
+ // entry, but external consumers also produce the category with `items: []`,
20
+ // so we treat presence of the category as the on-state.
21
+ return Boolean(category);
19
22
  }
20
23
 
21
24
  if (schema.type === 'multi-select') {
@@ -31,15 +31,13 @@ import {
31
31
  type TextStyleDefinition,
32
32
  } from '../../tokens';
33
33
  import { resolveColorToken } from '../../utils/color-props';
34
+ import type { TypographyStyleProp } from '../../utils/typography-props';
34
35
 
35
36
  import type { TextColor } from '../../tokens';
36
37
 
37
- /** Typography styling props that are controlled by the variant and should not be set directly */
38
- type TypographyStyleProps = 'fw' | 'fz' | 'size' | 'lh' | 'ff' | 'lts' | 'fs';
39
-
40
38
  export type TextProps = Omit<
41
39
  MantineTextProps,
42
- TypographyStyleProps | 'c' | 'children'
40
+ TypographyStyleProp | 'c' | 'children'
43
41
  > & {
44
42
  /** Text style variant from the design system. Defaults to 'body1'. */
45
43
  variant?: BodyVariant;
@@ -24,12 +24,10 @@ import {
24
24
  type HeadingVariant,
25
25
  } from '../../tokens/text-styles';
26
26
  import { resolveColorToken } from '../../utils/color-props';
27
+ import type { TypographyStyleProp } from '../../utils/typography-props';
27
28
 
28
29
  import type { TextColor } from '../../tokens/color-types';
29
30
 
30
- /** Typography styling props that are controlled by the variant and should not be set directly */
31
- type TypographyStyleProps = 'fw' | 'fz' | 'size' | 'lh' | 'ff' | 'lts' | 'fs';
32
-
33
31
  const variantToOrder: Record<HeadingVariant, TitleOrder> = {
34
32
  heading1: 1,
35
33
  heading2: 2,
@@ -40,7 +38,7 @@ const variantToOrder: Record<HeadingVariant, TitleOrder> = {
40
38
 
41
39
  export type TitleProps = Omit<
42
40
  MantineTitleProps,
43
- TypographyStyleProps | 'c' | 'children' | 'order'
41
+ TypographyStyleProp | 'c' | 'children' | 'order'
44
42
  > & {
45
43
  /** Heading style variant from the design system. */
46
44
  variant?: HeadingVariant;
package/src/index.ts CHANGED
@@ -30,7 +30,7 @@ export type {
30
30
  FilterCategory,
31
31
  } from './components/AppliedFiltersManagerBar';
32
32
  export { Anchor } from './components/Anchor';
33
- export type { AnchorProps } from './components/Anchor';
33
+ export type { AnchorProps, AnchorSize, AnchorTone } from './components/Anchor';
34
34
  export { Badge } from './components/Badge';
35
35
  export type { BadgeProps } from './components/Badge';
36
36
  export { BulkActionBar } from './components/BulkActionBar';
@@ -0,0 +1,21 @@
1
+ // Inject the Inter font from Google Fonts as a `<link rel="stylesheet">`.
2
+ //
3
+ // Loading the stylesheet via JS instead of `import './foo.css'` keeps the
4
+ // design system robust against consumer bundlers that don't resolve `.css`
5
+ // files inside `node_modules/@scalepad/ui`. Idempotent — safe to import
6
+ // from multiple entry points.
7
+
8
+ const INTER_FONT_LINK_ID = 'scalepad-ui-inter-font';
9
+ const INTER_FONT_HREF =
10
+ 'https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap';
11
+
12
+ if (
13
+ typeof document !== 'undefined' &&
14
+ !document.getElementById(INTER_FONT_LINK_ID)
15
+ ) {
16
+ const link = document.createElement('link');
17
+ link.id = INTER_FONT_LINK_ID;
18
+ link.rel = 'stylesheet';
19
+ link.href = INTER_FONT_HREF;
20
+ document.head.appendChild(link);
21
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Mantine typography style props that the design system intentionally locks
3
+ * down. Components driven by a `variant` (e.g. `Text`, `Title`, `Anchor`)
4
+ * Omit this set from their public props so callers can't bypass the variant
5
+ * system with one-off `fz="20px"` or `fw={700}` overrides.
6
+ *
7
+ * Internally those components also avoid forwarding these props to the
8
+ * underlying Mantine element — the variant is the only path that can set
9
+ * typography.
10
+ */
11
+ export type TypographyStyleProp =
12
+ | 'fw'
13
+ | 'fz'
14
+ | 'size'
15
+ | 'lh'
16
+ | 'ff'
17
+ | 'lts'
18
+ | 'fs'
19
+ | 'tt';
@@ -1,48 +0,0 @@
1
- // Import Geist font files as URLs
2
- import geistSansItalicFont from './assets/Geist-Italic[wght].ttf?url';
3
- import geistSansFont from './assets/Geist[wght].ttf?url';
4
- import geistMonoItalicFont from './assets/GeistMono-Italic[wght].ttf?url';
5
- import geistMonoFont from './assets/GeistMono[wght].ttf?url';
6
-
7
- // Inject @font-face rules (only once)
8
- if (
9
- typeof document !== 'undefined' &&
10
- !document.getElementById('geist-fonts')
11
- ) {
12
- const style = document.createElement('style');
13
- style.id = 'geist-fonts';
14
- style.textContent = `
15
- @font-face {
16
- font-family: 'Geist Sans';
17
- src: url('${geistSansFont}') format('truetype');
18
- font-weight: 100 900;
19
- font-style: normal;
20
- font-display: swap;
21
- }
22
-
23
- @font-face {
24
- font-family: 'Geist Sans';
25
- src: url('${geistSansItalicFont}') format('truetype');
26
- font-weight: 100 900;
27
- font-style: italic;
28
- font-display: swap;
29
- }
30
-
31
- @font-face {
32
- font-family: 'Geist Mono';
33
- src: url('${geistMonoFont}') format('truetype');
34
- font-weight: 100 900;
35
- font-style: normal;
36
- font-display: swap;
37
- }
38
-
39
- @font-face {
40
- font-family: 'Geist Mono';
41
- src: url('${geistMonoItalicFont}') format('truetype');
42
- font-weight: 100 900;
43
- font-style: italic;
44
- font-display: swap;
45
- }
46
- `;
47
- document.head.appendChild(style);
48
- }