@astryxdesign/native 0.7.5-canary.7e855c0 → 0.7.5-canary.85e48a0

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/apple/README.md CHANGED
@@ -51,17 +51,15 @@ apple/
51
51
  that also imports SwiftUI, write `Astryx.Button` and `SwiftUI.Button`. A
52
52
  component's parameters use core's prop names
53
53
  and values (`variant: .primary`, `size: .lg`, `isLoading`), so code reads
54
- the same across web and native. `Text` and `Heading` take the generated
55
- `TextProps` and `HeadingProps`, as AST-093 FR5 asks; the others keep five
56
- exceptions, the interim shape FR5 exempts until they move onto the generated
54
+ the same across web and native. `Text`, `Heading`, and `Card` take the
55
+ generated `TextProps`, `HeadingProps`, and `CardProps`, as AST-093 FR5 asks;
56
+ the others keep four exceptions, the interim shape FR5 exempts until they move onto the generated
57
57
  contracts:
58
58
  - `Button` takes `action` where core has `onClick`.
59
59
  - `Button` takes `fillsWidth` where core has `width="100%"`.
60
60
  - `Button` takes `icon` as a shared icon name (`"search"`), which the
61
61
  generated `ButtonProps` does not carry yet.
62
62
  - `TextInput` takes a `Binding` where core has `value` and `onChange`.
63
- - `Card` takes `padding` as a whole spacing step, so it cannot spell
64
- core's `0.5` and `1.5` steps.
65
63
  - **`Templates/`** holds one folder per page template, named after the
66
64
  `astryx template` id. Templates are clone-only, as on the web: you copy a
67
65
  template's folder into your app and own it from then on. They are a
@@ -1,33 +1,106 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
- import SwiftUI
4
-
5
- /// A card: one independent item on the theme's card ground, with a hairline
6
- /// border and the container radius.
3
+ /// A card: one independent item on the theme's card ground. It takes core's
4
+ /// Card props (`CardProps`) and stacks its content top to bottom, as children
5
+ /// flow inside core's Card.
7
6
  public struct Card<Content: View>: View {
8
- let padding: Int
7
+ public let props: CardProps
9
8
  let content: Content
10
9
  @Environment(\.astryx) private var astryx
11
10
 
12
- /// - Parameter padding: a step on the spacing ladder, as core's `padding`.
13
- public init(padding: Int = 4, @ViewBuilder content: () -> Content) {
14
- self.padding = padding
11
+ /// - Parameters:
12
+ /// - padding: core's `padding`, a step on the spacing scale; step 4 when
13
+ /// nil.
14
+ /// - variant: core's `variant`: the card ground, or `transparent` for none.
15
+ /// Only `default` draws a border.
16
+ /// - elevation: core's `elevation`, the theme's shadow at that depth.
17
+ /// - base: core's base props.
18
+ /// - content: core's `children`.
19
+ public init(
20
+ padding: CardPadding? = nil,
21
+ variant: CardVariant? = nil,
22
+ elevation: CardElevation? = nil,
23
+ base: AstryxBaseProps = AstryxBaseProps(),
24
+ @ViewBuilder content: () -> Content
25
+ ) {
26
+ self.props = CardProps(padding: padding, variant: variant, elevation: elevation, base: base)
15
27
  self.content = content()
16
28
  }
17
29
 
18
30
  public var body: some View {
19
- let shape = RoundedRectangle(
20
- cornerRadius: astryx.size("radius-container", 12),
21
- style: .continuous
22
- )
23
- // Several child views stack in one card, top to bottom with no gap of
24
- // their own, as children flow inside core's Card.
25
- VStack(alignment: .leading, spacing: 0) { content }
26
- .padding(astryx.space(padding))
31
+ let shape = RoundedRectangle(cornerRadius: astryx.tokens.radius.container, style: .continuous)
32
+ let variant = props.variant ?? .default
33
+ // The border sits inside the padding, so the content's inset is the
34
+ // padding step, as in core.
35
+ SwiftUI.VStack(alignment: .leading, spacing: 0) { content }
36
+ .padding(padding)
27
37
  .frame(maxWidth: .infinity, alignment: .leading)
28
- .background(shape.fill(astryx.color("color-background-card")))
29
- .overlay(
30
- shape.strokeBorder(astryx.color("color-border"), lineWidth: astryx.size("border-width", 1))
31
- )
38
+ .background(shape.fill(Self.ground(variant, astryx.tokens.color)))
39
+ .overlay {
40
+ if variant == .default {
41
+ shape.strokeBorder(astryx.tokens.color.border, lineWidth: astryx.tokens.size.borderWidth)
42
+ }
43
+ }
44
+ .astryxShadow(Self.shadow(props.elevation ?? .none, astryx.tokens.shadow), in: shape)
45
+ .astryxBase(props.base)
46
+ }
47
+
48
+ var padding: CGFloat {
49
+ let space = astryx.tokens.space
50
+ switch props.padding ?? .v4 {
51
+ case .v0: return space.s0
52
+ case .v0_5: return space.s0_5
53
+ case .v1: return space.s1
54
+ case .v1_5: return space.s1_5
55
+ case .v2: return space.s2
56
+ case .v3: return space.s3
57
+ case .v4: return space.s4
58
+ case .v5: return space.s5
59
+ case .v6: return space.s6
60
+ case .v8: return space.s8
61
+ case .v10: return space.s10
62
+ }
63
+ }
64
+
65
+ /// The ground core's Card draws for a variant.
66
+ static func ground(_ variant: CardVariant, _ color: ColorTokens) -> Color {
67
+ switch variant {
68
+ case .default: color.backgroundCard
69
+ case .transparent: .clear
70
+ case .muted: color.backgroundMuted
71
+ case .blue: color.backgroundBlue
72
+ case .cyan: color.backgroundCyan
73
+ case .gray: color.backgroundGray
74
+ case .green: color.backgroundGreen
75
+ case .orange: color.backgroundOrange
76
+ case .pink: color.backgroundPink
77
+ case .purple: color.backgroundPurple
78
+ case .red: color.backgroundRed
79
+ case .teal: color.backgroundTeal
80
+ case .yellow: color.backgroundYellow
81
+ }
82
+ }
83
+
84
+ /// The theme's shadow at an elevation.
85
+ static func shadow(_ elevation: CardElevation, _ shadow: ShadowTokens) -> [ShadowLayer] {
86
+ switch elevation {
87
+ case .none: []
88
+ case .low: shadow.low
89
+ case .med: shadow.med
90
+ case .high: shadow.high
91
+ }
92
+ }
93
+ }
94
+
95
+ extension Card where Content == AstryxNodeList {
96
+ /// A card from core's props, with its children as the wire tree a logic
97
+ /// core sends; `send` carries the children's callbacks back to the core.
98
+ public init(
99
+ _ props: CardProps,
100
+ children: [AstryxNode],
101
+ send: @escaping @Sendable (AstryxAction) -> Void
102
+ ) {
103
+ self.props = props
104
+ self.content = AstryxNodeList(nodes: children, send: send)
32
105
  }
33
106
  }
@@ -214,6 +214,11 @@ extension Theme {
214
214
  size: FontSizeTokens(base: 14, lg: 18, s2xl: 27, s2xs: 7, s3xl: 34, s3xs: 6, s4xl: 43, s4xs: 5, s5xl: 53, sm: 11, xl: 22, xs: 9),
215
215
  supporting: TypeStyleTokens(size: 12, weight: 400, leading: 1.4545),
216
216
  weight: FontWeightTokens(bold: 700, medium: 500, normal: 400, semibold: 600)
217
+ ),
218
+ shadow: ShadowTokens(
219
+ high: [ShadowLayer(x: 0, y: 4, blur: 6, color: Color(astryxRGBA: 0x1D1C111A)), ShadowLayer(x: 0, y: 12, blur: 24, color: Color(astryxRGBA: 0x1D1C1126))],
220
+ low: [ShadowLayer(x: 0, y: 2, blur: 4, color: Color(astryxRGBA: 0x1D1C110D)), ShadowLayer(x: 0, y: 4, blur: 8, color: Color(astryxRGBA: 0x1D1C111A))],
221
+ med: [ShadowLayer(x: 0, y: 2, blur: 4, color: Color(astryxRGBA: 0x1D1C110D)), ShadowLayer(x: 0, y: 4, blur: 12, color: Color(astryxRGBA: 0x1D1C111A))]
217
222
  )
218
223
  ),
219
224
  dark: Tokens(
@@ -423,6 +428,11 @@ extension Theme {
423
428
  size: FontSizeTokens(base: 14, lg: 18, s2xl: 27, s2xs: 7, s3xl: 34, s3xs: 6, s4xl: 43, s4xs: 5, s5xl: 53, sm: 11, xl: 22, xs: 9),
424
429
  supporting: TypeStyleTokens(size: 12, weight: 400, leading: 1.4545),
425
430
  weight: FontWeightTokens(bold: 700, medium: 500, normal: 400, semibold: 600)
431
+ ),
432
+ shadow: ShadowTokens(
433
+ high: [ShadowLayer(x: 0, y: 4, blur: 6, color: Color(astryxRGBA: 0x1D1C111A)), ShadowLayer(x: 0, y: 12, blur: 24, color: Color(astryxRGBA: 0x1D1C1126))],
434
+ low: [ShadowLayer(x: 0, y: 2, blur: 4, color: Color(astryxRGBA: 0x1D1C110D)), ShadowLayer(x: 0, y: 4, blur: 8, color: Color(astryxRGBA: 0x1D1C111A))],
435
+ med: [ShadowLayer(x: 0, y: 2, blur: 4, color: Color(astryxRGBA: 0x1D1C110D)), ShadowLayer(x: 0, y: 4, blur: 12, color: Color(astryxRGBA: 0x1D1C111A))]
426
436
  )
427
437
  )
428
438
  )
@@ -214,6 +214,11 @@ extension Theme {
214
214
  size: FontSizeTokens(base: 14, lg: 17, s2xl: 24, s2xs: 8, s3xl: 29, s3xs: 7, s4xl: 35, s4xs: 6, s5xl: 42, sm: 12, xl: 20, xs: 10),
215
215
  supporting: TypeStyleTokens(size: 12, weight: 400, leading: 1.6667),
216
216
  weight: FontWeightTokens(bold: 700, medium: 500, normal: 400, semibold: 600)
217
+ ),
218
+ shadow: ShadowTokens(
219
+ high: [ShadowLayer(x: 0, y: 4, blur: 6, color: Color(astryxRGBA: 0x4A35201A)), ShadowLayer(x: 0, y: 12, blur: 24, color: Color(astryxRGBA: 0x4A352026))],
220
+ low: [ShadowLayer(x: 0, y: 2, blur: 4, color: Color(astryxRGBA: 0x4A35200D)), ShadowLayer(x: 0, y: 4, blur: 8, color: Color(astryxRGBA: 0x4A35201A))],
221
+ med: [ShadowLayer(x: 0, y: 2, blur: 4, color: Color(astryxRGBA: 0x4A35200D)), ShadowLayer(x: 0, y: 4, blur: 12, color: Color(astryxRGBA: 0x4A35201A))]
217
222
  )
218
223
  ),
219
224
  dark: Tokens(
@@ -423,6 +428,11 @@ extension Theme {
423
428
  size: FontSizeTokens(base: 14, lg: 17, s2xl: 24, s2xs: 8, s3xl: 29, s3xs: 7, s4xl: 35, s4xs: 6, s5xl: 42, sm: 12, xl: 20, xs: 10),
424
429
  supporting: TypeStyleTokens(size: 12, weight: 400, leading: 1.6667),
425
430
  weight: FontWeightTokens(bold: 700, medium: 500, normal: 400, semibold: 600)
431
+ ),
432
+ shadow: ShadowTokens(
433
+ high: [ShadowLayer(x: 0, y: 4, blur: 6, color: Color(astryxRGBA: 0x4A35201A)), ShadowLayer(x: 0, y: 12, blur: 24, color: Color(astryxRGBA: 0x4A352026))],
434
+ low: [ShadowLayer(x: 0, y: 2, blur: 4, color: Color(astryxRGBA: 0x4A35200D)), ShadowLayer(x: 0, y: 4, blur: 8, color: Color(astryxRGBA: 0x4A35201A))],
435
+ med: [ShadowLayer(x: 0, y: 2, blur: 4, color: Color(astryxRGBA: 0x4A35200D)), ShadowLayer(x: 0, y: 4, blur: 12, color: Color(astryxRGBA: 0x4A35201A))]
426
436
  )
427
437
  )
428
438
  )
@@ -214,6 +214,11 @@ extension Theme {
214
214
  size: FontSizeTokens(base: 14, lg: 17, s2xl: 24, s2xs: 8, s3xl: 29, s3xs: 7, s4xl: 35, s4xs: 6, s5xl: 42, sm: 12, xl: 20, xs: 10),
215
215
  supporting: TypeStyleTokens(size: 12, weight: 400, leading: 1.6667),
216
216
  weight: FontWeightTokens(bold: 700, medium: 500, normal: 400, semibold: 600)
217
+ ),
218
+ shadow: ShadowTokens(
219
+ high: [ShadowLayer(x: 0, y: 2, blur: 2, color: Color(astryxRGBA: 0x0000001A)), ShadowLayer(x: 0, y: 8, blur: 24, color: Color(astryxRGBA: 0x0000001A))],
220
+ low: [ShadowLayer(x: 0, y: 1, blur: 1, color: Color(astryxRGBA: 0x0000001A)), ShadowLayer(x: 0, y: 2, blur: 8, color: Color(astryxRGBA: 0x0000001A))],
221
+ med: [ShadowLayer(x: 0, y: 1, blur: 2, color: Color(astryxRGBA: 0x0000001A)), ShadowLayer(x: 0, y: 2, blur: 12, color: Color(astryxRGBA: 0x0000001A))]
217
222
  )
218
223
  ),
219
224
  dark: Tokens(
@@ -423,6 +428,11 @@ extension Theme {
423
428
  size: FontSizeTokens(base: 14, lg: 17, s2xl: 24, s2xs: 8, s3xl: 29, s3xs: 7, s4xl: 35, s4xs: 6, s5xl: 42, sm: 12, xl: 20, xs: 10),
424
429
  supporting: TypeStyleTokens(size: 12, weight: 400, leading: 1.6667),
425
430
  weight: FontWeightTokens(bold: 700, medium: 500, normal: 400, semibold: 600)
431
+ ),
432
+ shadow: ShadowTokens(
433
+ high: [ShadowLayer(x: 0, y: 2, blur: 2, color: Color(astryxRGBA: 0x00000033)), ShadowLayer(x: 0, y: 8, blur: 24, color: Color(astryxRGBA: 0x0000004D))],
434
+ low: [ShadowLayer(x: 0, y: 1, blur: 1, color: Color(astryxRGBA: 0x00000033)), ShadowLayer(x: 0, y: 2, blur: 8, color: Color(astryxRGBA: 0x00000033))],
435
+ med: [ShadowLayer(x: 0, y: 1, blur: 2, color: Color(astryxRGBA: 0x00000033)), ShadowLayer(x: 0, y: 2, blur: 12, color: Color(astryxRGBA: 0x00000033))]
426
436
  )
427
437
  )
428
438
  )
@@ -214,6 +214,11 @@ extension Theme {
214
214
  size: FontSizeTokens(base: 16, lg: 20, s2xl: 31, s2xs: 8, s3xl: 39, s3xs: 7, s4xl: 49, s4xs: 5, s5xl: 61, sm: 13, xl: 25, xs: 10),
215
215
  supporting: TypeStyleTokens(size: 13, weight: 400, leading: 1.5385),
216
216
  weight: FontWeightTokens(bold: 700, medium: 500, normal: 400, semibold: 600)
217
+ ),
218
+ shadow: ShadowTokens(
219
+ high: [ShadowLayer(x: 0, y: 4, blur: 6, color: Color(astryxRGBA: 0x00000040)), ShadowLayer(x: 0, y: 12, blur: 24, color: Color(astryxRGBA: 0x0000004D))],
220
+ low: [ShadowLayer(x: 0, y: 2, blur: 4, color: Color(astryxRGBA: 0x00000033)), ShadowLayer(x: 0, y: 4, blur: 8, color: Color(astryxRGBA: 0x00000040))],
221
+ med: [ShadowLayer(x: 0, y: 2, blur: 4, color: Color(astryxRGBA: 0x00000033)), ShadowLayer(x: 0, y: 4, blur: 12, color: Color(astryxRGBA: 0x00000040))]
217
222
  )
218
223
  ),
219
224
  dark: Tokens(
@@ -423,6 +428,11 @@ extension Theme {
423
428
  size: FontSizeTokens(base: 16, lg: 20, s2xl: 31, s2xs: 8, s3xl: 39, s3xs: 7, s4xl: 49, s4xs: 5, s5xl: 61, sm: 13, xl: 25, xs: 10),
424
429
  supporting: TypeStyleTokens(size: 13, weight: 400, leading: 1.5385),
425
430
  weight: FontWeightTokens(bold: 700, medium: 500, normal: 400, semibold: 600)
431
+ ),
432
+ shadow: ShadowTokens(
433
+ high: [ShadowLayer(x: 0, y: 4, blur: 6, color: Color(astryxRGBA: 0x00000040)), ShadowLayer(x: 0, y: 12, blur: 24, color: Color(astryxRGBA: 0x0000004D))],
434
+ low: [ShadowLayer(x: 0, y: 2, blur: 4, color: Color(astryxRGBA: 0x00000033)), ShadowLayer(x: 0, y: 4, blur: 8, color: Color(astryxRGBA: 0x00000040))],
435
+ med: [ShadowLayer(x: 0, y: 2, blur: 4, color: Color(astryxRGBA: 0x00000033)), ShadowLayer(x: 0, y: 4, blur: 12, color: Color(astryxRGBA: 0x00000040))]
426
436
  )
427
437
  )
428
438
  )
@@ -214,6 +214,11 @@ extension Theme {
214
214
  size: FontSizeTokens(base: 16, lg: 20, s2xl: 31, s2xs: 8, s3xl: 39, s3xs: 7, s4xl: 49, s4xs: 5, s5xl: 61, sm: 13, xl: 25, xs: 10),
215
215
  supporting: TypeStyleTokens(size: 13, weight: 400, leading: 1.5385),
216
216
  weight: FontWeightTokens(bold: 700, medium: 500, normal: 400, semibold: 600)
217
+ ),
218
+ shadow: ShadowTokens(
219
+ high: [ShadowLayer(x: 0, y: 4, blur: 6, color: Color(astryxRGBA: 0x3E481D1A)), ShadowLayer(x: 0, y: 12, blur: 24, color: Color(astryxRGBA: 0x3E481D26))],
220
+ low: [ShadowLayer(x: 0, y: 2, blur: 4, color: Color(astryxRGBA: 0x3E481D0D)), ShadowLayer(x: 0, y: 4, blur: 8, color: Color(astryxRGBA: 0x3E481D1A))],
221
+ med: [ShadowLayer(x: 0, y: 2, blur: 4, color: Color(astryxRGBA: 0x3E481D0D)), ShadowLayer(x: 0, y: 4, blur: 12, color: Color(astryxRGBA: 0x3E481D1A))]
217
222
  )
218
223
  ),
219
224
  dark: Tokens(
@@ -423,6 +428,11 @@ extension Theme {
423
428
  size: FontSizeTokens(base: 16, lg: 20, s2xl: 31, s2xs: 8, s3xl: 39, s3xs: 7, s4xl: 49, s4xs: 5, s5xl: 61, sm: 13, xl: 25, xs: 10),
424
429
  supporting: TypeStyleTokens(size: 13, weight: 400, leading: 1.5385),
425
430
  weight: FontWeightTokens(bold: 700, medium: 500, normal: 400, semibold: 600)
431
+ ),
432
+ shadow: ShadowTokens(
433
+ high: [ShadowLayer(x: 0, y: 4, blur: 6, color: Color(astryxRGBA: 0x3E481D1A)), ShadowLayer(x: 0, y: 12, blur: 24, color: Color(astryxRGBA: 0x3E481D26))],
434
+ low: [ShadowLayer(x: 0, y: 2, blur: 4, color: Color(astryxRGBA: 0x3E481D0D)), ShadowLayer(x: 0, y: 4, blur: 8, color: Color(astryxRGBA: 0x3E481D1A))],
435
+ med: [ShadowLayer(x: 0, y: 2, blur: 4, color: Color(astryxRGBA: 0x3E481D0D)), ShadowLayer(x: 0, y: 4, blur: 12, color: Color(astryxRGBA: 0x3E481D1A))]
426
436
  )
427
437
  )
428
438
  )
@@ -214,6 +214,11 @@ extension Theme {
214
214
  size: FontSizeTokens(base: 14, lg: 17, s2xl: 24, s2xs: 8, s3xl: 29, s3xs: 7, s4xl: 35, s4xs: 6, s5xl: 42, sm: 12, xl: 20, xs: 10),
215
215
  supporting: TypeStyleTokens(size: 12, weight: 400, leading: 1.6667),
216
216
  weight: FontWeightTokens(bold: 700, medium: 500, normal: 400, semibold: 600)
217
+ ),
218
+ shadow: ShadowTokens(
219
+ high: [ShadowLayer(x: 0, y: 4, blur: 6, color: Color(astryxRGBA: 0x0000001A)), ShadowLayer(x: 0, y: 12, blur: 24, color: Color(astryxRGBA: 0x00000026)), ShadowLayer(x: 0, y: 0, blur: 0, spread: 1, isInset: true, color: Color(astryxRGBA: 0x00000000))],
220
+ low: [ShadowLayer(x: 0, y: 2, blur: 4, color: Color(astryxRGBA: 0x0000000D)), ShadowLayer(x: 0, y: 4, blur: 8, color: Color(astryxRGBA: 0x0000001A)), ShadowLayer(x: 0, y: 0, blur: 0, spread: 1, isInset: true, color: Color(astryxRGBA: 0x00000000))],
221
+ med: [ShadowLayer(x: 0, y: 2, blur: 4, color: Color(astryxRGBA: 0x0000000D)), ShadowLayer(x: 0, y: 4, blur: 12, color: Color(astryxRGBA: 0x0000001A)), ShadowLayer(x: 0, y: 0, blur: 0, spread: 1, isInset: true, color: Color(astryxRGBA: 0x00000000))]
217
222
  )
218
223
  ),
219
224
  dark: Tokens(
@@ -423,6 +428,11 @@ extension Theme {
423
428
  size: FontSizeTokens(base: 14, lg: 17, s2xl: 24, s2xs: 8, s3xl: 29, s3xs: 7, s4xl: 35, s4xs: 6, s5xl: 42, sm: 12, xl: 20, xs: 10),
424
429
  supporting: TypeStyleTokens(size: 12, weight: 400, leading: 1.6667),
425
430
  weight: FontWeightTokens(bold: 700, medium: 500, normal: 400, semibold: 600)
431
+ ),
432
+ shadow: ShadowTokens(
433
+ high: [ShadowLayer(x: 0, y: 4, blur: 6, color: Color(astryxRGBA: 0x00000080)), ShadowLayer(x: 0, y: 12, blur: 24, color: Color(astryxRGBA: 0x000000B3)), ShadowLayer(x: 0, y: 0, blur: 0, spread: 1, isInset: true, color: Color(astryxRGBA: 0xFFFFFF26))],
434
+ low: [ShadowLayer(x: 0, y: 2, blur: 4, color: Color(astryxRGBA: 0x00000040)), ShadowLayer(x: 0, y: 4, blur: 8, color: Color(astryxRGBA: 0x00000066)), ShadowLayer(x: 0, y: 0, blur: 0, spread: 1, isInset: true, color: Color(astryxRGBA: 0xFFFFFF14))],
435
+ med: [ShadowLayer(x: 0, y: 2, blur: 4, color: Color(astryxRGBA: 0x00000059)), ShadowLayer(x: 0, y: 4, blur: 12, color: Color(astryxRGBA: 0x00000080)), ShadowLayer(x: 0, y: 0, blur: 0, spread: 1, isInset: true, color: Color(astryxRGBA: 0xFFFFFF1F))]
426
436
  )
427
437
  )
428
438
  )
@@ -214,6 +214,11 @@ extension Theme {
214
214
  size: FontSizeTokens(base: 14, lg: 18, s2xl: 27, s2xs: 7, s3xl: 34, s3xs: 6, s4xl: 43, s4xs: 5, s5xl: 53, sm: 11, xl: 22, xs: 9),
215
215
  supporting: TypeStyleTokens(size: 12, weight: 400, leading: 1.4545),
216
216
  weight: FontWeightTokens(bold: 700, medium: 500, normal: 400, semibold: 600)
217
+ ),
218
+ shadow: ShadowTokens(
219
+ high: [ShadowLayer(x: 0, y: 4, blur: 6, color: Color(astryxRGBA: 0x25252A1A)), ShadowLayer(x: 0, y: 12, blur: 24, color: Color(astryxRGBA: 0x25252A26))],
220
+ low: [ShadowLayer(x: 0, y: 2, blur: 4, color: Color(astryxRGBA: 0x25252A0D)), ShadowLayer(x: 0, y: 4, blur: 8, color: Color(astryxRGBA: 0x25252A1A))],
221
+ med: [ShadowLayer(x: 0, y: 2, blur: 4, color: Color(astryxRGBA: 0x25252A0D)), ShadowLayer(x: 0, y: 4, blur: 12, color: Color(astryxRGBA: 0x25252A1A))]
217
222
  )
218
223
  ),
219
224
  dark: Tokens(
@@ -423,6 +428,11 @@ extension Theme {
423
428
  size: FontSizeTokens(base: 14, lg: 18, s2xl: 27, s2xs: 7, s3xl: 34, s3xs: 6, s4xl: 43, s4xs: 5, s5xl: 53, sm: 11, xl: 22, xs: 9),
424
429
  supporting: TypeStyleTokens(size: 12, weight: 400, leading: 1.4545),
425
430
  weight: FontWeightTokens(bold: 700, medium: 500, normal: 400, semibold: 600)
431
+ ),
432
+ shadow: ShadowTokens(
433
+ high: [ShadowLayer(x: 0, y: 4, blur: 6, color: Color(astryxRGBA: 0x25252A1A)), ShadowLayer(x: 0, y: 12, blur: 24, color: Color(astryxRGBA: 0x25252A26))],
434
+ low: [ShadowLayer(x: 0, y: 2, blur: 4, color: Color(astryxRGBA: 0x25252A0D)), ShadowLayer(x: 0, y: 4, blur: 8, color: Color(astryxRGBA: 0x25252A1A))],
435
+ med: [ShadowLayer(x: 0, y: 2, blur: 4, color: Color(astryxRGBA: 0x25252A0D)), ShadowLayer(x: 0, y: 4, blur: 12, color: Color(astryxRGBA: 0x25252A1A))]
426
436
  )
427
437
  )
428
438
  )
@@ -11,19 +11,44 @@ public struct Tokens: Equatable, Sendable {
11
11
  public var radius: RadiusTokens
12
12
  public var size: SizeTokens
13
13
  public var font: FontTokens
14
+ public var shadow: ShadowTokens
14
15
 
15
16
  public init(
16
17
  color: ColorTokens,
17
18
  space: SpaceTokens,
18
19
  radius: RadiusTokens,
19
20
  size: SizeTokens,
20
- font: FontTokens
21
+ font: FontTokens,
22
+ shadow: ShadowTokens
21
23
  ) {
22
24
  self.color = color
23
25
  self.space = space
24
26
  self.radius = radius
25
27
  self.size = size
26
28
  self.font = font
29
+ self.shadow = shadow
30
+ }
31
+ }
32
+
33
+ /// One layer of a theme's shadow, as CSS `box-shadow` draws it: a drop
34
+ /// shadow (an offset and a blur), or an inset ring `spread` points wide.
35
+ public struct ShadowLayer: Equatable, Sendable {
36
+ public var x: CGFloat
37
+ public var y: CGFloat
38
+ public var blur: CGFloat
39
+ public var spread: CGFloat
40
+ public var isInset: Bool
41
+ public var color: Color
42
+
43
+ public init(
44
+ x: CGFloat, y: CGFloat, blur: CGFloat, spread: CGFloat = 0, isInset: Bool = false, color: Color
45
+ ) {
46
+ self.x = x
47
+ self.y = y
48
+ self.blur = blur
49
+ self.spread = spread
50
+ self.isInset = isInset
51
+ self.color = color
27
52
  }
28
53
  }
29
54
 
@@ -1260,3 +1285,34 @@ public struct FontWeightTokens: Equatable, Sendable {
1260
1285
  self.semibold = semibold
1261
1286
  }
1262
1287
  }
1288
+
1289
+ /// The theme's `shadow` tokens, one typed member per token.
1290
+ public struct ShadowTokens: Equatable, Sendable {
1291
+ /// `--shadow-high`
1292
+ public var high: [ShadowLayer]
1293
+ /// `--shadow-low`
1294
+ public var low: [ShadowLayer]
1295
+ /// `--shadow-med`
1296
+ public var med: [ShadowLayer]
1297
+
1298
+ public init(
1299
+ high: [ShadowLayer],
1300
+ low: [ShadowLayer],
1301
+ med: [ShadowLayer]
1302
+ ) {
1303
+ self.high = high
1304
+ self.low = low
1305
+ self.med = med
1306
+ }
1307
+
1308
+ /// A token by its CSS name, for code that only has the name. Components
1309
+ /// read the typed members instead.
1310
+ func value(named token: String) -> [ShadowLayer]? {
1311
+ switch token {
1312
+ case "shadow-high": high
1313
+ case "shadow-low": low
1314
+ case "shadow-med": med
1315
+ default: nil
1316
+ }
1317
+ }
1318
+ }
@@ -214,6 +214,11 @@ extension Theme {
214
214
  size: FontSizeTokens(base: 16, lg: 20, s2xl: 31, s2xs: 8, s3xl: 39, s3xs: 7, s4xl: 49, s4xs: 5, s5xl: 61, sm: 13, xl: 25, xs: 10),
215
215
  supporting: TypeStyleTokens(size: 12, weight: 400, leading: 1.5385),
216
216
  weight: FontWeightTokens(bold: 700, medium: 500, normal: 400, semibold: 600)
217
+ ),
218
+ shadow: ShadowTokens(
219
+ high: [ShadowLayer(x: 0, y: 4, blur: 6, color: Color(astryxRGBA: 0x2D241B1A)), ShadowLayer(x: 0, y: 12, blur: 24, color: Color(astryxRGBA: 0x2D241B26))],
220
+ low: [ShadowLayer(x: 0, y: 2, blur: 4, color: Color(astryxRGBA: 0x2D241B0D)), ShadowLayer(x: 0, y: 4, blur: 8, color: Color(astryxRGBA: 0x2D241B1A))],
221
+ med: [ShadowLayer(x: 0, y: 2, blur: 4, color: Color(astryxRGBA: 0x2D241B0D)), ShadowLayer(x: 0, y: 4, blur: 12, color: Color(astryxRGBA: 0x2D241B1A))]
217
222
  )
218
223
  ),
219
224
  dark: Tokens(
@@ -423,6 +428,11 @@ extension Theme {
423
428
  size: FontSizeTokens(base: 16, lg: 20, s2xl: 31, s2xs: 8, s3xl: 39, s3xs: 7, s4xl: 49, s4xs: 5, s5xl: 61, sm: 13, xl: 25, xs: 10),
424
429
  supporting: TypeStyleTokens(size: 12, weight: 400, leading: 1.5385),
425
430
  weight: FontWeightTokens(bold: 700, medium: 500, normal: 400, semibold: 600)
431
+ ),
432
+ shadow: ShadowTokens(
433
+ high: [ShadowLayer(x: 0, y: 4, blur: 6, color: Color(astryxRGBA: 0x2D241B1A)), ShadowLayer(x: 0, y: 12, blur: 24, color: Color(astryxRGBA: 0x2D241B26))],
434
+ low: [ShadowLayer(x: 0, y: 2, blur: 4, color: Color(astryxRGBA: 0x2D241B0D)), ShadowLayer(x: 0, y: 4, blur: 8, color: Color(astryxRGBA: 0x2D241B1A))],
435
+ med: [ShadowLayer(x: 0, y: 2, blur: 4, color: Color(astryxRGBA: 0x2D241B0D)), ShadowLayer(x: 0, y: 4, blur: 12, color: Color(astryxRGBA: 0x2D241B1A))]
426
436
  )
427
437
  )
428
438
  )
@@ -0,0 +1,39 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ extension View {
4
+ /// Draws a theme shadow around this view's shape, as CSS `box-shadow` does:
5
+ /// drop layers only outside the shape, so a see-through view shows no
6
+ /// shadow beneath it, and inset rings inside its edge.
7
+ func astryxShadow<S: InsettableShape>(_ layers: [ShadowLayer], in shape: S) -> some View {
8
+ background {
9
+ ZStack {
10
+ ForEach(Array(layers.enumerated()), id: \.offset) { _, layer in
11
+ if !layer.isInset {
12
+ // A CSS blur is twice SwiftUI's shadow radius.
13
+ shape.fill(.black)
14
+ .shadow(color: layer.color, radius: layer.blur / 2, x: layer.x, y: layer.y)
15
+ }
16
+ }
17
+ }
18
+ .mask {
19
+ // Everything but the shape itself.
20
+ Rectangle()
21
+ .padding(-Self.shadowReach(layers))
22
+ .overlay(shape.fill(.black).blendMode(.destinationOut))
23
+ .compositingGroup()
24
+ }
25
+ }
26
+ .overlay {
27
+ ForEach(Array(layers.enumerated()), id: \.offset) { _, layer in
28
+ if layer.isInset {
29
+ shape.strokeBorder(layer.color, lineWidth: layer.spread)
30
+ }
31
+ }
32
+ }
33
+ }
34
+
35
+ /// How far past the shape the layers can draw.
36
+ fileprivate static func shadowReach(_ layers: [ShadowLayer]) -> CGFloat {
37
+ layers.map { $0.blur + max(abs($0.x), abs($0.y)) }.max().map { $0 + 4 } ?? 0
38
+ }
39
+ }