@ponti-studios/ui 1.0.0 → 1.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ponti-studios/ui",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "description": "Shared UI component library for Ponti Studios",
6
6
  "license": "MIT",
@@ -133,13 +133,15 @@
133
133
  "style-dictionary": "5.5.0",
134
134
  "tailwindcss": "^4.3.3",
135
135
  "typescript": "6.0.3",
136
+ "uniwind": "^1.10.1",
136
137
  "vite": "^8.1.5",
137
138
  "vitest": "^4.1.10"
138
139
  },
139
140
  "peerDependencies": {
140
141
  "react": "^18.2.0 || ^19.0.0",
141
142
  "react-dom": "^18.2.0 || ^19.0.0",
142
- "react-native": ">=0.74"
143
+ "react-native": ">=0.74",
144
+ "uniwind": ">=1.10.0"
143
145
  },
144
146
  "peerDependenciesMeta": {
145
147
  "react-dom": {
@@ -147,6 +149,9 @@
147
149
  },
148
150
  "react-native": {
149
151
  "optional": true
152
+ },
153
+ "uniwind": {
154
+ "optional": true
150
155
  }
151
156
  },
152
157
  "packageManager": "pnpm@11.10.0"
@@ -5,7 +5,6 @@ import {
5
5
  DropdownMenu,
6
6
  DropdownMenuContent,
7
7
  DropdownMenuGroup,
8
- DropdownMenuItem,
9
8
  DropdownMenuLabel,
10
9
  DropdownMenuSeparator,
11
10
  DropdownMenuTrigger,
@@ -57,10 +56,9 @@ export function SortControls({
57
56
  .filter((_, i) => i !== index)
58
57
  .map((option) => option.field);
59
58
  return (
60
- <DropdownMenuItem
59
+ <div
61
60
  key={sort.field}
62
- onSelect={(e) => e.preventDefault()}
63
- className={`p-0 focus:bg-transparent ${index === focusedSortIndex ? "bg-muted" : ""}`}
61
+ className={`rounded-sm ${index === focusedSortIndex ? "bg-muted" : ""}`}
64
62
  >
65
63
  <SortRow
66
64
  sortOption={sort}
@@ -70,27 +68,26 @@ export function SortControls({
70
68
  updateSortOption={updateSortOption}
71
69
  removeSortOption={removeSortOption}
72
70
  />
73
- </DropdownMenuItem>
71
+ </div>
74
72
  );
75
73
  })}
76
74
  </DropdownMenuGroup>
77
75
  {availableFieldsToAdd.length > 0 && (
78
76
  <>
79
77
  <DropdownMenuSeparator />
80
- <DropdownMenuItem
81
- onSelect={(e) => {
82
- e.preventDefault();
78
+ <button
79
+ type="button"
80
+ onClick={() => {
83
81
  const firstAvailable = availableFieldsToAdd[0];
84
82
  if (firstAvailable) {
85
83
  addSortOption({ field: firstAvailable, direction: "desc" });
86
84
  }
87
85
  }}
88
- disabled={availableFieldsToAdd.length === 0}
89
- className="mt-1"
86
+ className="hover:bg-muted focus-visible:ring-ring mt-1 flex w-full cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus-visible:ring-2 focus-visible:ring-offset-2"
90
87
  >
91
88
  <PlusCircle className="mr-2 size-4" />
92
89
  Add Sort Criterion
93
- </DropdownMenuItem>
90
+ </button>
94
91
  </>
95
92
  )}
96
93
  </DropdownMenuContent>
@@ -1,7 +1,13 @@
1
+ import { ArrowDownWideNarrow, ArrowUpWideNarrow, X } from "lucide-react";
2
+
1
3
  import type { SortField, SortOption } from "../../hooks/sort.types";
2
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../forms/select";
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuTrigger,
9
+ } from "../overlays/dropdown-menu";
3
10
  import { Button } from "../primitives/button";
4
- import { X } from "lucide-react";
5
11
 
6
12
  export interface SortRowProps {
7
13
  sortOption: SortOption;
@@ -12,6 +18,10 @@ export interface SortRowProps {
12
18
  removeSortOption: (index: number) => void;
13
19
  }
14
20
 
21
+ function fieldLabel(field: SortField) {
22
+ return field.charAt(0).toUpperCase() + field.slice(1);
23
+ }
24
+
15
25
  export function SortRow({
16
26
  sortOption,
17
27
  index,
@@ -25,47 +35,51 @@ export function SortRow({
25
35
  );
26
36
 
27
37
  return (
28
- <div key={sortOption.field} className="flex items-center gap-2">
29
- <Select
30
- value={sortOption.field}
31
- onValueChange={(value) => updateSortOption(index, { ...sortOption, field: value })}
32
- >
33
- <SelectTrigger className="h-8 w-[130px] text-xs">
34
- <SelectValue placeholder="Select field" />
35
- </SelectTrigger>
36
- <SelectContent>
38
+ <div className="flex w-full items-center gap-1">
39
+ <DropdownMenu>
40
+ <DropdownMenuTrigger asChild>
41
+ <button
42
+ type="button"
43
+ className="hover:bg-muted focus-visible:ring-ring flex-1 rounded-md px-2 py-1.5 text-left text-sm outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
44
+ >
45
+ {fieldLabel(sortOption.field)}
46
+ </button>
47
+ </DropdownMenuTrigger>
48
+ <DropdownMenuContent>
37
49
  {availableFieldsForCurrentSelect.map((field) => (
38
- <SelectItem key={field} value={field} className="text-xs">
39
- {field.charAt(0).toUpperCase() + field.slice(1)}
40
- </SelectItem>
50
+ <DropdownMenuItem
51
+ key={field}
52
+ onClick={() => updateSortOption(index, { ...sortOption, field })}
53
+ >
54
+ {fieldLabel(field)}
55
+ </DropdownMenuItem>
41
56
  ))}
42
- </SelectContent>
43
- </Select>
44
- <Select
45
- value={sortOption.direction}
46
- onValueChange={(value) => {
47
- if (value === "asc" || value === "desc") {
48
- updateSortOption(index, { ...sortOption, direction: value });
49
- }
50
- }}
57
+ </DropdownMenuContent>
58
+ </DropdownMenu>
59
+ <Button
60
+ variant="ghost"
61
+ size="sm"
62
+ className="size-8 p-0"
63
+ aria-label={sortOption.direction === "asc" ? "Sorted ascending" : "Sorted descending"}
64
+ onClick={() =>
65
+ updateSortOption(index, {
66
+ ...sortOption,
67
+ direction: sortOption.direction === "asc" ? "desc" : "asc",
68
+ })
69
+ }
51
70
  >
52
- <SelectTrigger className="h-8 w-[110px] text-xs">
53
- <SelectValue placeholder="Direction" />
54
- </SelectTrigger>
55
- <SelectContent>
56
- <SelectItem value="asc" className="text-xs">
57
- Ascending
58
- </SelectItem>
59
- <SelectItem value="desc" className="text-xs">
60
- Descending
61
- </SelectItem>
62
- </SelectContent>
63
- </Select>
71
+ {sortOption.direction === "asc" ? (
72
+ <ArrowUpWideNarrow className="size-4" />
73
+ ) : (
74
+ <ArrowDownWideNarrow className="size-4" />
75
+ )}
76
+ </Button>
64
77
  <Button
65
78
  variant="ghost"
66
79
  size="sm"
67
80
  onClick={() => removeSortOption(index)}
68
81
  className="size-8 p-0"
82
+ aria-label="Remove sort criterion"
69
83
  >
70
84
  <X className="text-muted-foreground size-4" />
71
85
  </Button>
@@ -15,7 +15,7 @@ export function PaginationControls({
15
15
  onPageChange,
16
16
  }: PaginationControlsProps) {
17
17
  return (
18
- <div className="border-border bg-background text-foreground inline-flex items-center overflow-hidden rounded-full border text-sm">
18
+ <div className="border-border bg-background text-foreground inline-flex w-fit shrink-0 items-center self-start overflow-hidden rounded-full border text-sm">
19
19
  <Button
20
20
  type="button"
21
21
  variant="outline"
@@ -49,18 +49,28 @@ const SheetOverlay = React.forwardRef<
49
49
  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Backdrop>
50
50
  >(({ className, ...props }, ref) => (
51
51
  <SheetPrimitive.Backdrop
52
- className={cn(
53
- "data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 fixed inset-0 z-50 bg-black/80 transition-opacity",
54
- className,
55
- )}
52
+ data-slot="sheet-overlay"
53
+ className={cn("fixed inset-0 z-50 bg-black/80", className)}
56
54
  {...props}
57
55
  ref={ref}
58
56
  />
59
57
  ));
60
58
  SheetOverlay.displayName = "SheetOverlay";
61
59
 
62
- const sheetClassName =
63
- "bg-background text-foreground data-open:animate-in data-closed:animate-out fixed inset-x-0 bottom-0 z-50 flex max-h-[90dvh] min-h-[30dvh] flex-col gap-6 rounded-t-xl border border-b-0 border-l-0 border-r-0 p-4 pb-[max(1rem,env(safe-area-inset-bottom))] md:inset-y-0 md:right-0 md:bottom-auto md:left-auto md:h-full md:w-3/4 md:max-w-sm md:min-h-0 md:rounded-t-none md:rounded-l-xl md:border-t-0 md:border-r-0 md:border-b-0 md:border-l md:p-6 md:pb-6";
60
+ const sheetClassName = [
61
+ // Appearance
62
+ "bg-background text-foreground rounded-xl border shadow-lg",
63
+ // Layout
64
+ "flex flex-col gap-6 p-4",
65
+ // Position: floats off every edge, anchored to the bottom on mobile
66
+ "fixed z-50 inset-x-2 bottom-[max(0.5rem,env(safe-area-inset-bottom))]",
67
+ // Size: mobile
68
+ "max-h-[calc(100dvh-2rem)] min-h-[30dvh]",
69
+ // Position: anchored to the right edge on desktop
70
+ "md:inset-y-2 md:right-2 md:left-auto md:bottom-auto",
71
+ // Size: desktop
72
+ "md:w-3/4 md:max-w-sm md:min-h-0",
73
+ ];
64
74
 
65
75
  interface SheetContentProps extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Popup> {}
66
76
 
@@ -70,7 +80,12 @@ const SheetContent = React.forwardRef<
70
80
  >(({ className, children, ...props }, ref) => (
71
81
  <SheetPortal>
72
82
  <SheetOverlay />
73
- <SheetPrimitive.Popup ref={ref} className={cn(sheetClassName, className)} {...props}>
83
+ <SheetPrimitive.Popup
84
+ ref={ref}
85
+ data-slot="sheet-content"
86
+ className={cn(sheetClassName, className)}
87
+ {...props}
88
+ >
74
89
  <SheetPrimitive.Close className="text-muted-foreground hover:bg-muted hover:text-muted-foreground focus-visible:outline-ring absolute top-3 right-3 inline-flex size-9 items-center justify-center rounded-md border border-transparent transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 disabled:pointer-events-none md:top-4 md:right-4">
75
90
  <X className="size-4.5" />
76
91
  <span className="sr-only">Close</span>
@@ -0,0 +1,34 @@
1
+ import * as React from "react";
2
+ import { Text, View, type TextProps, type ViewProps } from "react-native";
3
+
4
+ import { cn } from "../lib/utils";
5
+
6
+ function Card({ className, ...props }: ViewProps) {
7
+ return <View className={cn("bg-card gap-4 rounded border border-border p-4", className)} {...props} />;
8
+ }
9
+
10
+ function CardHeader({ className, ...props }: ViewProps) {
11
+ return <View className={cn("gap-1", className)} {...props} />;
12
+ }
13
+
14
+ function CardTitle({ className, ...props }: TextProps) {
15
+ return <Text className={cn("text-card-foreground text-xl", className)} {...props} />;
16
+ }
17
+
18
+ function CardDescription({ className, ...props }: TextProps) {
19
+ return <Text className={cn("text-muted-foreground text-xs", className)} {...props} />;
20
+ }
21
+
22
+ function CardAction({ className, ...props }: ViewProps) {
23
+ return <View className={cn("self-start", className)} {...props} />;
24
+ }
25
+
26
+ function CardContent({ className, ...props }: ViewProps) {
27
+ return <View className={className} {...props} />;
28
+ }
29
+
30
+ function CardFooter({ className, ...props }: ViewProps) {
31
+ return <View className={cn("flex-row items-center", className)} {...props} />;
32
+ }
33
+
34
+ export { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };
@@ -0,0 +1,41 @@
1
+ import type { ReactNode } from "react";
2
+ import { Pressable } from "react-native";
3
+ import { useCSSVariable } from "uniwind";
4
+
5
+ interface IconButtonProps {
6
+ accessibilityLabel?: string;
7
+ children: ReactNode;
8
+ disabled?: boolean;
9
+ onPress?: () => void;
10
+ testID?: string;
11
+ variant?: "bordered" | "plain";
12
+ }
13
+
14
+ export function IconButton({
15
+ accessibilityLabel,
16
+ children,
17
+ disabled = false,
18
+ onPress,
19
+ testID,
20
+ variant = "bordered",
21
+ }: IconButtonProps) {
22
+ const [border] = useCSSVariable(["--color-border"]) as string[];
23
+
24
+ return (
25
+ <Pressable
26
+ accessibilityLabel={accessibilityLabel}
27
+ accessibilityRole="button"
28
+ disabled={disabled}
29
+ onPress={onPress}
30
+ testID={testID}
31
+ className={`w-8 h-8 rounded-full border items-center justify-center ${variant === "plain" ? "border-0" : ""}`}
32
+ style={({ pressed }) => [
33
+ { borderColor: variant === "plain" ? "transparent" : border },
34
+ pressed && { opacity: 0.7 },
35
+ disabled && { opacity: 0.4 },
36
+ ]}
37
+ >
38
+ {children}
39
+ </Pressable>
40
+ );
41
+ }
@@ -9,3 +9,20 @@
9
9
  export { createMakeStyles, type NamedStyles } from "./make-styles";
10
10
  export { nativeShadows, type NativeShadowLayer } from "./shadows";
11
11
  export { useColorMode, useThemeColors } from "./use-theme-colors";
12
+ export { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./card";
13
+ export { TextField } from "./text-field";
14
+ export { IconButton } from "./icon-button";
15
+
16
+ export {
17
+ typography as nativeTypography,
18
+ spacing as nativeSpacing,
19
+ screenMargin as nativeScreenMargin,
20
+ componentSize as nativeComponentSize,
21
+ borderRadius as nativeBorderRadius,
22
+ streamItem as nativeStreamItem,
23
+ fontFamily as nativeFontFamily,
24
+ type TypographyVariant,
25
+ type SpacingToken,
26
+ type ComponentSizeToken,
27
+ type BorderRadiusToken,
28
+ } from "../styles/tokens/normalize-native";
@@ -0,0 +1,34 @@
1
+ import { forwardRef, useState } from "react";
2
+ import { TextInput, type TextInputProps } from "react-native";
3
+ import { useCSSVariable } from "uniwind";
4
+
5
+ export const TextField = forwardRef<TextInput, TextInputProps>(function TextField(
6
+ { onBlur, onFocus, style, ...props },
7
+ ref,
8
+ ) {
9
+ const [ring, borderInput, foreground, mutedForeground] = useCSSVariable([
10
+ "--color-ring",
11
+ "--color-input",
12
+ "--color-foreground",
13
+ "--color-muted-foreground",
14
+ ]) as string[];
15
+ const [focused, setFocused] = useState(false);
16
+
17
+ return (
18
+ <TextInput
19
+ {...props}
20
+ ref={ref}
21
+ onBlur={(event) => {
22
+ setFocused(false);
23
+ onBlur?.(event);
24
+ }}
25
+ onFocus={(event) => {
26
+ setFocused(true);
27
+ onFocus?.(event);
28
+ }}
29
+ placeholderTextColor={props.placeholderTextColor ?? mutedForeground}
30
+ className="rounded-md border min-h-11 px-4 py-3 text-base"
31
+ style={[{ borderColor: focused ? ring : borderInput, color: foreground }, style]}
32
+ />
33
+ );
34
+ });
@@ -0,0 +1 @@
1
+ /// <reference types="uniwind/types" />
@@ -50,6 +50,46 @@
50
50
  transform: scale(0.95);
51
51
  }
52
52
  }
53
+ @keyframes sheet-slide-in-bottom {
54
+ from {
55
+ transform: translateY(100%);
56
+ opacity: 0;
57
+ }
58
+ to {
59
+ transform: translateY(0);
60
+ opacity: 1;
61
+ }
62
+ }
63
+ @keyframes sheet-slide-out-bottom {
64
+ from {
65
+ transform: translateY(0);
66
+ opacity: 1;
67
+ }
68
+ to {
69
+ transform: translateY(100%);
70
+ opacity: 0;
71
+ }
72
+ }
73
+ @keyframes sheet-slide-in-right {
74
+ from {
75
+ transform: translateX(100%);
76
+ opacity: 0;
77
+ }
78
+ to {
79
+ transform: translateX(0);
80
+ opacity: 1;
81
+ }
82
+ }
83
+ @keyframes sheet-slide-out-right {
84
+ from {
85
+ transform: translateX(0);
86
+ opacity: 1;
87
+ }
88
+ to {
89
+ transform: translateX(100%);
90
+ opacity: 0;
91
+ }
92
+ }
53
93
 
54
94
  .data-open\:animate-in[data-open],
55
95
  .data-open\:animate-in[data-state="open"] {
@@ -64,6 +104,38 @@
64
104
  zoom-out 120ms ease-in;
65
105
  }
66
106
 
107
+ [data-slot="sheet-overlay"][data-open],
108
+ [data-slot="sheet-overlay"][data-state="open"] {
109
+ animation: fade-in 400ms var(--ease-out);
110
+ }
111
+ [data-slot="sheet-overlay"][data-closed],
112
+ [data-slot="sheet-overlay"][data-state="closed"] {
113
+ /* Must stay in lockstep with [data-slot="sheet-content"]'s close duration below (both 200ms) —
114
+ Base UI gates the whole portal's unmount on this element's own close-animation duration, not
115
+ the content panel's, so a mismatch here yanks the sliding panel off mid-animation. */
116
+ animation: fade-out 200ms var(--ease-in);
117
+ }
118
+
119
+ [data-slot="sheet-content"][data-open],
120
+ [data-slot="sheet-content"][data-state="open"] {
121
+ animation: sheet-slide-in-bottom 300ms var(--ease-drawer);
122
+ }
123
+ [data-slot="sheet-content"][data-closed],
124
+ [data-slot="sheet-content"][data-state="closed"] {
125
+ animation: sheet-slide-out-bottom 200ms var(--ease-drawer);
126
+ }
127
+
128
+ @media (min-width: 768px) {
129
+ [data-slot="sheet-content"][data-open],
130
+ [data-slot="sheet-content"][data-state="open"] {
131
+ animation: sheet-slide-in-right 300ms var(--ease-drawer);
132
+ }
133
+ [data-slot="sheet-content"][data-closed],
134
+ [data-slot="sheet-content"][data-state="closed"] {
135
+ animation: sheet-slide-out-right 200ms var(--ease-drawer);
136
+ }
137
+ }
138
+
67
139
  @media (prefers-reduced-motion: reduce) {
68
140
  .animate-in,
69
141
  .animate-out,
@@ -72,3 +144,22 @@
72
144
  animation: none !important;
73
145
  }
74
146
  }
147
+
148
+ @media (prefers-reduced-motion: reduce) {
149
+ [data-slot="sheet-overlay"][data-open],
150
+ [data-slot="sheet-overlay"][data-state="open"] {
151
+ animation: fade-in 120ms var(--ease-out) !important;
152
+ }
153
+ [data-slot="sheet-overlay"][data-closed],
154
+ [data-slot="sheet-overlay"][data-state="closed"] {
155
+ animation: fade-out 120ms var(--ease-in) !important;
156
+ }
157
+ [data-slot="sheet-content"][data-open],
158
+ [data-slot="sheet-content"][data-state="open"] {
159
+ animation: fade-in var(--duration-150) var(--ease-out) !important;
160
+ }
161
+ [data-slot="sheet-content"][data-closed],
162
+ [data-slot="sheet-content"][data-state="closed"] {
163
+ animation: fade-out 120ms var(--ease-in) !important;
164
+ }
165
+ }
@@ -43,14 +43,16 @@
43
43
  animation-name: zoom-out;
44
44
  }
45
45
 
46
- /* shadcn New York semantic contract. Values are generated from the DTCG
47
- * source; this file only names the roles consumed by web components. */
46
+ /*
47
+ Values are generated from the DTCG source;
48
+ this file only names the roles consumed by web components.
49
+ */
48
50
  @theme inline {
49
51
  --color-background: var(--background);
50
52
  --color-foreground: var(--text-primary);
51
53
  --color-card: var(--card);
52
54
  --color-card-foreground: var(--text-primary);
53
- --color-popover: var(--popover);
55
+ --color-popover: var(--background);
54
56
  --color-popover-foreground: var(--text-primary);
55
57
  --color-primary: var(--primary);
56
58
  --color-primary-foreground: var(--primary-foreground);
@@ -133,6 +133,7 @@
133
133
  --ease-in: cubic-bezier(0.4, 0, 1, 1);
134
134
  --ease-out: cubic-bezier(0, 0, 0.2, 1);
135
135
  --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
136
+ --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
136
137
  --duration-75: 75ms;
137
138
  --duration-100: 100ms;
138
139
  --duration-150: 150ms;
@@ -431,6 +431,7 @@ export default {
431
431
  in: [0.4, 0, 1, 1],
432
432
  out: [0, 0, 0.2, 1],
433
433
  "in-out": [0.4, 0, 0.2, 1],
434
+ drawer: [0.32, 0.72, 0, 1],
434
435
  },
435
436
  duration: {
436
437
  75: "75ms",
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Do not edit directly, this file was auto-generated.
3
+ */
4
+
5
+ export default {
6
+ text: {
7
+ "large-title": {
8
+ fontSize: {
9
+ value: 34,
10
+ unit: "px",
11
+ },
12
+ lineHeight: {
13
+ value: 41,
14
+ unit: "px",
15
+ },
16
+ fontWeight: 700,
17
+ letterSpacing: {
18
+ value: -0.6,
19
+ unit: "px",
20
+ },
21
+ },
22
+ title1: {
23
+ fontSize: {
24
+ value: 28,
25
+ unit: "px",
26
+ },
27
+ lineHeight: {
28
+ value: 34,
29
+ unit: "px",
30
+ },
31
+ fontWeight: 700,
32
+ letterSpacing: {
33
+ value: -0.4,
34
+ unit: "px",
35
+ },
36
+ },
37
+ title2: {
38
+ fontSize: {
39
+ value: 22,
40
+ unit: "px",
41
+ },
42
+ lineHeight: {
43
+ value: 28,
44
+ unit: "px",
45
+ },
46
+ fontWeight: 600,
47
+ letterSpacing: {
48
+ value: -0.2,
49
+ unit: "px",
50
+ },
51
+ },
52
+ headline: {
53
+ fontSize: {
54
+ value: 17,
55
+ unit: "px",
56
+ },
57
+ lineHeight: {
58
+ value: 22,
59
+ unit: "px",
60
+ },
61
+ fontWeight: 600,
62
+ letterSpacing: {
63
+ value: -0.1,
64
+ unit: "px",
65
+ },
66
+ },
67
+ body: {
68
+ fontSize: {
69
+ value: 17,
70
+ unit: "px",
71
+ },
72
+ lineHeight: {
73
+ value: 24,
74
+ unit: "px",
75
+ },
76
+ fontWeight: 400,
77
+ letterSpacing: {
78
+ value: 0,
79
+ unit: "px",
80
+ },
81
+ },
82
+ callout: {
83
+ fontSize: {
84
+ value: 16,
85
+ unit: "px",
86
+ },
87
+ lineHeight: {
88
+ value: 22,
89
+ unit: "px",
90
+ },
91
+ fontWeight: 400,
92
+ letterSpacing: {
93
+ value: -0.1,
94
+ unit: "px",
95
+ },
96
+ },
97
+ subhead: {
98
+ fontSize: {
99
+ value: 15,
100
+ unit: "px",
101
+ },
102
+ lineHeight: {
103
+ value: 20,
104
+ unit: "px",
105
+ },
106
+ fontWeight: 400,
107
+ letterSpacing: {
108
+ value: 0,
109
+ unit: "px",
110
+ },
111
+ },
112
+ footnote: {
113
+ fontSize: {
114
+ value: 13,
115
+ unit: "px",
116
+ },
117
+ lineHeight: {
118
+ value: 18,
119
+ unit: "px",
120
+ },
121
+ fontWeight: 400,
122
+ letterSpacing: {
123
+ value: 0,
124
+ unit: "px",
125
+ },
126
+ },
127
+ caption1: {
128
+ fontSize: {
129
+ value: 12,
130
+ unit: "px",
131
+ },
132
+ lineHeight: {
133
+ value: 16,
134
+ unit: "px",
135
+ },
136
+ fontWeight: 400,
137
+ letterSpacing: {
138
+ value: 0,
139
+ unit: "px",
140
+ },
141
+ },
142
+ caption2: {
143
+ fontSize: {
144
+ value: 11,
145
+ unit: "px",
146
+ },
147
+ lineHeight: {
148
+ value: 14,
149
+ unit: "px",
150
+ },
151
+ fontWeight: 500,
152
+ letterSpacing: {
153
+ value: 0.2,
154
+ unit: "px",
155
+ },
156
+ },
157
+ display: {
158
+ fontSize: {
159
+ value: 40,
160
+ unit: "px",
161
+ },
162
+ lineHeight: {
163
+ value: 44,
164
+ unit: "px",
165
+ },
166
+ fontWeight: 700,
167
+ letterSpacing: {
168
+ value: -1.2,
169
+ unit: "px",
170
+ },
171
+ },
172
+ overline: {
173
+ fontSize: {
174
+ value: 11,
175
+ unit: "px",
176
+ },
177
+ lineHeight: {
178
+ value: 16,
179
+ unit: "px",
180
+ },
181
+ fontWeight: 500,
182
+ letterSpacing: {
183
+ value: 0.8,
184
+ unit: "px",
185
+ },
186
+ textTransform: "uppercase",
187
+ },
188
+ mono: {
189
+ fontFamily: "Menlo",
190
+ fontSize: {
191
+ value: 12,
192
+ unit: "px",
193
+ },
194
+ lineHeight: {
195
+ value: 16,
196
+ unit: "px",
197
+ },
198
+ fontWeight: 400,
199
+ letterSpacing: {
200
+ value: 0,
201
+ unit: "px",
202
+ },
203
+ },
204
+ },
205
+ space: {
206
+ sm: "0.5rem",
207
+ md: "0.75rem",
208
+ lg: "1rem",
209
+ xl: "1.5rem",
210
+ },
211
+ "screen-margin": "1rem",
212
+ "component-size": {
213
+ sm: "1rem",
214
+ md: "1.5rem",
215
+ lg: "2rem",
216
+ xl: "2.75rem",
217
+ icon: "1.25rem",
218
+ },
219
+ "border-radius": {
220
+ sm: "0.25rem",
221
+ md: "0.375rem",
222
+ lg: "0.5rem",
223
+ icon: "0.375rem",
224
+ },
225
+ "stream-item": {
226
+ "border-radius": "0.375rem",
227
+ "border-width": "1px",
228
+ gap: "0.5rem",
229
+ "icon-size": "1.5rem",
230
+ "min-height": "50px",
231
+ "padding-horizontal": "0.5rem",
232
+ "padding-vertical": "0.25rem",
233
+ },
234
+ "font-family": {
235
+ primary: "System",
236
+ mono: "Menlo",
237
+ },
238
+ };
@@ -0,0 +1,86 @@
1
+ import nativeGenerated from "./generated/native.generated";
2
+
3
+ const toPixels = (value: string) => {
4
+ if (value.endsWith("rem")) return Number.parseFloat(value) * 16;
5
+ if (value.endsWith("px")) return Number.parseFloat(value);
6
+ throw new Error(`Unsupported dimension: ${value}`);
7
+ };
8
+
9
+ const unwrap = (v: { value: number; unit?: string } | number) =>
10
+ typeof v === "number" ? v : v.value;
11
+
12
+ type DimensionValue = number | { value: number; unit?: string };
13
+
14
+ const makeFlat = (raw: Record<string, unknown>) => ({
15
+ fontFamily: raw.fontFamily as string | undefined,
16
+ fontSize: unwrap(raw.fontSize as DimensionValue),
17
+ fontWeight: raw.fontWeight as number,
18
+ lineHeight: unwrap(raw.lineHeight as DimensionValue),
19
+ letterSpacing: unwrap(raw.letterSpacing as DimensionValue),
20
+ ...(raw.textTransform ? { textTransform: raw.textTransform as "uppercase" | "lowercase" | "capitalize" | "none" } : {}),
21
+ });
22
+
23
+ const generated = nativeGenerated as unknown as {
24
+ text: Record<string, Record<string, unknown>>;
25
+ space: Record<string, string>;
26
+ "screen-margin": string;
27
+ "component-size": Record<string, string>;
28
+ "border-radius": Record<string, string>;
29
+ "stream-item": Record<string, string>;
30
+ "font-family": { primary: string; mono: string };
31
+ };
32
+
33
+ const primaryFont = generated["font-family"].primary;
34
+
35
+ const typographyEntries = Object.entries(generated.text).map(([key, raw]) => {
36
+ const flat = makeFlat(raw);
37
+ if (!flat.fontFamily) flat.fontFamily = primaryFont;
38
+ return [key, flat] as const;
39
+ });
40
+
41
+ export const typography = Object.fromEntries(typographyEntries) as Record<
42
+ string,
43
+ {
44
+ fontFamily: string;
45
+ fontSize: number;
46
+ fontWeight: number;
47
+ lineHeight: number;
48
+ letterSpacing: number;
49
+ textTransform?: "uppercase" | "lowercase" | "capitalize" | "none";
50
+ }
51
+ >;
52
+
53
+ export type TypographyVariant = keyof typeof typography;
54
+
55
+ export const spacing = Object.fromEntries(
56
+ Object.entries(generated.space).map(([key, value]) => [key, toPixels(value)]),
57
+ ) as Record<keyof typeof generated.space, number>;
58
+
59
+ export type SpacingToken = keyof typeof spacing;
60
+
61
+ export const screenMargin = toPixels(generated["screen-margin"]);
62
+
63
+ export const componentSize = Object.fromEntries(
64
+ Object.entries(generated["component-size"]).map(([key, value]) => [key, toPixels(value)]),
65
+ ) as Record<keyof typeof generated["component-size"], number>;
66
+
67
+ export type ComponentSizeToken = keyof typeof componentSize;
68
+
69
+ export const borderRadius = Object.fromEntries(
70
+ Object.entries(generated["border-radius"]).map(([key, value]) => [key, toPixels(value)]),
71
+ ) as Record<keyof typeof generated["border-radius"], number>;
72
+
73
+ export type BorderRadiusToken = keyof typeof borderRadius;
74
+
75
+ const rawStreamItem = generated["stream-item"];
76
+ export const streamItem = {
77
+ borderRadius: toPixels(rawStreamItem["border-radius"]),
78
+ borderWidth: toPixels(rawStreamItem["border-width"]),
79
+ gap: toPixels(rawStreamItem.gap),
80
+ iconSize: toPixels(rawStreamItem["icon-size"]),
81
+ minHeight: toPixels(rawStreamItem["min-height"]),
82
+ paddingHorizontal: toPixels(rawStreamItem["padding-horizontal"]),
83
+ paddingVertical: toPixels(rawStreamItem["padding-vertical"]),
84
+ };
85
+
86
+ export const fontFamily = generated["font-family"];
@@ -318,7 +318,8 @@
318
318
  "$type": "cubicBezier",
319
319
  "in": { "$value": [0.4, 0, 1, 1] },
320
320
  "out": { "$value": [0, 0, 0.2, 1] },
321
- "in-out": { "$value": [0.4, 0, 0.2, 1] }
321
+ "in-out": { "$value": [0.4, 0, 0.2, 1] },
322
+ "drawer": { "$value": [0.32, 0.72, 0, 1] }
322
323
  },
323
324
  "duration": {
324
325
  "$type": "duration",
@@ -0,0 +1,169 @@
1
+ {
2
+ "$extensions": {
3
+ "org.ponti.description": "Semantic and component tokens for React Native consumers. Values reference the shared foundations so mobile stays in lockstep with web. Consumed via @ponti-studios/ui/native."
4
+ },
5
+ "space": {
6
+ "$type": "dimension",
7
+ "sm": { "$value": "{spacing-scale.2}" },
8
+ "md": { "$value": "{spacing-scale.3}" },
9
+ "lg": { "$value": "{spacing-scale.4}" },
10
+ "xl": { "$value": "{spacing-scale.6}" }
11
+ },
12
+ "screen-margin": {
13
+ "$type": "dimension",
14
+ "$value": "{spacing-scale.4}"
15
+ },
16
+ "component-size": {
17
+ "$type": "dimension",
18
+ "sm": { "$value": "{spacing-scale.4}" },
19
+ "md": { "$value": "{spacing-scale.6}" },
20
+ "lg": { "$value": "{spacing-scale.8}" },
21
+ "xl": { "$value": "{spacing-scale.11}" },
22
+ "icon": { "$value": "{spacing-scale.5}" }
23
+ },
24
+ "border-radius": {
25
+ "$type": "dimension",
26
+ "sm": { "$value": "{radius.sm}" },
27
+ "md": { "$value": "{radius.md}" },
28
+ "lg": { "$value": "{radius.lg}" },
29
+ "icon": { "$value": "{radius.md}" }
30
+ },
31
+ "stream-item": {
32
+ "$description": "Shared look for stream rows (time, inbox). Mirrors what the app used to hand-roll in theme.ts.",
33
+ "$type": "dimension",
34
+ "border-radius": { "$value": "{radius.md}" },
35
+ "border-width": { "$value": { "value": 1, "unit": "px" } },
36
+ "gap": { "$value": "{spacing-scale.2}" },
37
+ "icon-size": { "$value": "{component-size.md}" },
38
+ "min-height": { "$value": { "value": 50, "unit": "px" } },
39
+ "padding-horizontal": { "$value": "{spacing-scale.2}" },
40
+ "padding-vertical": { "$value": "{spacing-scale.1}" }
41
+ },
42
+ "font-family": {
43
+ "$type": "string",
44
+ "primary": { "$value": "System" },
45
+ "mono": { "$value": "Menlo" }
46
+ },
47
+ "text": {
48
+ "$description": "Apple-HIG-aligned type ramp for native editorial text.",
49
+ "large-title": {
50
+ "$type": "typography",
51
+ "$value": {
52
+ "fontSize": { "value": 34, "unit": "px" },
53
+ "lineHeight": { "value": 41, "unit": "px" },
54
+ "fontWeight": "{font-weight.bold}",
55
+ "letterSpacing": { "value": -0.6, "unit": "px" }
56
+ }
57
+ },
58
+ "title1": {
59
+ "$type": "typography",
60
+ "$value": {
61
+ "fontSize": { "value": 28, "unit": "px" },
62
+ "lineHeight": { "value": 34, "unit": "px" },
63
+ "fontWeight": "{font-weight.bold}",
64
+ "letterSpacing": { "value": -0.4, "unit": "px" }
65
+ }
66
+ },
67
+ "title2": {
68
+ "$type": "typography",
69
+ "$value": {
70
+ "fontSize": { "value": 22, "unit": "px" },
71
+ "lineHeight": { "value": 28, "unit": "px" },
72
+ "fontWeight": "{font-weight.semibold}",
73
+ "letterSpacing": { "value": -0.2, "unit": "px" }
74
+ }
75
+ },
76
+ "headline": {
77
+ "$type": "typography",
78
+ "$value": {
79
+ "fontSize": { "value": 17, "unit": "px" },
80
+ "lineHeight": { "value": 22, "unit": "px" },
81
+ "fontWeight": "{font-weight.semibold}",
82
+ "letterSpacing": { "value": -0.1, "unit": "px" }
83
+ }
84
+ },
85
+ "body": {
86
+ "$type": "typography",
87
+ "$value": {
88
+ "fontSize": { "value": 17, "unit": "px" },
89
+ "lineHeight": { "value": 24, "unit": "px" },
90
+ "fontWeight": "{font-weight.normal}",
91
+ "letterSpacing": { "value": 0, "unit": "px" }
92
+ }
93
+ },
94
+ "callout": {
95
+ "$type": "typography",
96
+ "$value": {
97
+ "fontSize": { "value": 16, "unit": "px" },
98
+ "lineHeight": { "value": 22, "unit": "px" },
99
+ "fontWeight": "{font-weight.normal}",
100
+ "letterSpacing": { "value": -0.1, "unit": "px" }
101
+ }
102
+ },
103
+ "subhead": {
104
+ "$type": "typography",
105
+ "$value": {
106
+ "fontSize": { "value": 15, "unit": "px" },
107
+ "lineHeight": { "value": 20, "unit": "px" },
108
+ "fontWeight": "{font-weight.normal}",
109
+ "letterSpacing": { "value": 0, "unit": "px" }
110
+ }
111
+ },
112
+ "footnote": {
113
+ "$type": "typography",
114
+ "$value": {
115
+ "fontSize": { "value": 13, "unit": "px" },
116
+ "lineHeight": { "value": 18, "unit": "px" },
117
+ "fontWeight": "{font-weight.normal}",
118
+ "letterSpacing": { "value": 0, "unit": "px" }
119
+ }
120
+ },
121
+ "caption1": {
122
+ "$type": "typography",
123
+ "$value": {
124
+ "fontSize": { "value": 12, "unit": "px" },
125
+ "lineHeight": { "value": 16, "unit": "px" },
126
+ "fontWeight": "{font-weight.normal}",
127
+ "letterSpacing": { "value": 0, "unit": "px" }
128
+ }
129
+ },
130
+ "caption2": {
131
+ "$type": "typography",
132
+ "$value": {
133
+ "fontSize": { "value": 11, "unit": "px" },
134
+ "lineHeight": { "value": 14, "unit": "px" },
135
+ "fontWeight": "{font-weight.medium}",
136
+ "letterSpacing": { "value": 0.2, "unit": "px" }
137
+ }
138
+ },
139
+ "display": {
140
+ "$type": "typography",
141
+ "$value": {
142
+ "fontSize": { "value": 40, "unit": "px" },
143
+ "lineHeight": { "value": 44, "unit": "px" },
144
+ "fontWeight": "{font-weight.bold}",
145
+ "letterSpacing": { "value": -1.2, "unit": "px" }
146
+ }
147
+ },
148
+ "overline": {
149
+ "$type": "typography",
150
+ "$value": {
151
+ "fontSize": { "value": 11, "unit": "px" },
152
+ "lineHeight": { "value": 16, "unit": "px" },
153
+ "fontWeight": "{font-weight.medium}",
154
+ "letterSpacing": { "value": 0.8, "unit": "px" },
155
+ "textTransform": "uppercase"
156
+ }
157
+ },
158
+ "mono": {
159
+ "$type": "typography",
160
+ "$value": {
161
+ "fontFamily": "{font-family.mono}",
162
+ "fontSize": { "value": 12, "unit": "px" },
163
+ "lineHeight": { "value": 16, "unit": "px" },
164
+ "fontWeight": "{font-weight.normal}",
165
+ "letterSpacing": { "value": 0, "unit": "px" }
166
+ }
167
+ }
168
+ }
169
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Emits ONLY the native semantic/component tokens (native.tokens.json) as a
3
+ * typed module. Foundations are kept in the dictionary so references resolve,
4
+ * but are filtered out of the output — they already ship via
5
+ * foundations.generated.ts.
6
+ */
7
+ export default {
8
+ source: ["src/styles/tokens/source/foundations.tokens.json", "src/styles/tokens/source/native.tokens.json"],
9
+ platforms: {
10
+ typescript: {
11
+ transformGroup: "js",
12
+ buildPath: "src/styles/tokens/generated/",
13
+ files: [
14
+ {
15
+ destination: "native.generated.ts",
16
+ format: "javascript/esm",
17
+ options: { minify: true },
18
+ filter: (token) => token.filePath.includes("native.tokens.json"),
19
+ },
20
+ ],
21
+ },
22
+ },
23
+ };