@shipload/item-renderer 1.0.0-next.4 → 1.0.0-next.41

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.
@@ -1,22 +1,23 @@
1
1
  import type {ResolvedItem} from '@shipload/sdk'
2
- import {getStatDefinitions, categoryColors, displayName} from '@shipload/sdk'
2
+ import {getStatDefinitions, categoryColors, formatLocation, starsForStat} from '@shipload/sdk'
3
3
  import type {CargoItem} from '../payload/codec.ts'
4
4
  import {panel} from '../primitives/panel.ts'
5
- import {iconHex} from '../primitives/icon-hex.ts'
6
- import {text} from '../primitives/text.ts'
7
- import {divider} from '../primitives/divider.ts'
5
+ import {resourceIcon} from '../primitives/resource-icon.ts'
8
6
  import {statBar} from '../primitives/stat-bar.ts'
9
7
  import {quantityBadge} from '../primitives/quantity-badge.ts'
10
8
  import {tokens} from '../tokens/index.ts'
11
- import {shortCode, formatMass, tierBorder} from './_shared.ts'
12
-
13
- const CATEGORY_LABELS: Record<string, string> = {
14
- ore: 'Ore',
15
- crystal: 'Crystal',
16
- gas: 'Gas',
17
- regolith: 'Regolith',
18
- biomass: 'Biomass',
19
- }
9
+ import {
10
+ formatMass,
11
+ tierBorder,
12
+ metaRowBlock,
13
+ titleText,
14
+ BADGE_Y,
15
+ HEADER_H,
16
+ ICON_Y,
17
+ STAT_BLOCK_GAP,
18
+ STAT_ROW_H,
19
+ BOTTOM_PAD,
20
+ } from './_shared.ts'
20
21
 
21
22
  function categoryColor(category?: string): string {
22
23
  if (!category) return tokens.colors.text.muted
@@ -26,6 +27,7 @@ function categoryColor(category?: string): string {
26
27
 
27
28
  export interface RenderResourceOpts {
28
29
  mode?: 'values' | 'ranges'
30
+ location?: {x: number; y: number}
29
31
  }
30
32
 
31
33
  type StatRow = {
@@ -69,83 +71,53 @@ export function renderResource(
69
71
  }))
70
72
  }
71
73
 
72
- const headerH = 48
73
- const metaRowH = 28
74
- const statsH = rows.length * 26 + 8
75
- const height = headerH + metaRowH + 14 + statsH + pad
74
+ const metaRows = [
75
+ ...(opts?.location ? [{label: 'Location', value: formatLocation(opts.location)}] : []),
76
+ ]
76
77
 
77
- const chrome = panel({width: w, height, borderColor: tierBorder(resolved.tier)})
78
+ const metaYStart = pad + HEADER_H
79
+ const {svg: metaSvg, height: metaH} = metaRowBlock(pad, metaYStart, innerW, metaRows)
80
+ const statsYStart = metaYStart + metaH + STAT_BLOCK_GAP
81
+ const statsBottom =
82
+ statsYStart + Math.max(0, rows.length - 1) * STAT_ROW_H + tokens.spacing.statBarHeight
83
+ const height = statsBottom + BOTTOM_PAD
78
84
 
79
- const quantity = Number(BigInt(item.quantity.toString()))
80
- const badge = quantityBadge({x: w - pad, y: pad, quantity})
81
-
82
- const icon = iconHex({
83
- x: pad,
84
- y: pad + 4,
85
- color: categoryColor(resolved.category),
86
- code: shortCode(resolved.itemId),
87
- })
85
+ const chrome = panel({width: w, height, borderColor: tierBorder(resolved.tier)})
88
86
 
89
- const name = text({
90
- x: pad + 34,
91
- y: pad + 22,
92
- value: displayName(resolved),
93
- size: tokens.typography.sizes.title,
94
- weight: 700,
95
- family: tokens.typography.display,
96
- })
87
+ const identity = categoryColor(resolved.category)
97
88
 
98
- const catLabel = resolved.category ? (CATEGORY_LABELS[resolved.category] ?? 'Item') : 'Item'
99
- const catText = text({
100
- x: pad,
101
- y: pad + headerH + 4,
102
- value: 'Category',
103
- size: tokens.typography.sizes.body,
104
- color: tokens.colors.text.secondary,
105
- })
106
- const catValue = text({
107
- x: w - pad,
108
- y: pad + headerH + 4,
109
- value: catLabel,
110
- size: tokens.typography.sizes.body,
111
- weight: 600,
112
- anchor: 'end',
113
- })
114
- const massLabel = text({
115
- x: pad,
116
- y: pad + headerH + metaRowH - 8,
117
- value: 'Mass',
118
- size: tokens.typography.sizes.body,
119
- color: tokens.colors.text.secondary,
120
- })
121
- const massValue = text({
89
+ const quantity = Number(BigInt(item.quantity.toString()))
90
+ const badge = quantityBadge({
122
91
  x: w - pad,
123
- y: pad + headerH + metaRowH - 8,
124
- value: formatMass(resolved.mass),
125
- size: tokens.typography.sizes.body,
126
- weight: 600,
127
- anchor: 'end',
92
+ y: pad + BADGE_Y,
93
+ quantity,
94
+ label: formatMass(quantity * resolved.mass),
95
+ tone: identity,
128
96
  })
129
97
 
130
- const sepY = pad + headerH + metaRowH + 6
131
- const sep = divider({x: pad, y: sepY, width: innerW})
98
+ const icon = resolved.category
99
+ ? resourceIcon(resolved.category, {x: pad, y: pad + ICON_Y - 2, size: 28})
100
+ : ''
101
+
102
+ const name = titleText(pad + 34, pad + 22, resolved)
132
103
 
133
104
  const statsSvg = rows
134
105
  .map((row, i) =>
135
106
  statBar({
136
107
  x: pad,
137
- y: sepY + 18 + i * 26,
108
+ y: statsYStart + i * STAT_ROW_H,
138
109
  width: innerW,
139
110
  label: row.label,
140
111
  abbreviation: row.abbreviation,
141
112
  value: row.value,
142
113
  color: row.color,
143
114
  inverted: row.inverted,
115
+ stars: row.value !== null ? starsForStat(row.value) : undefined,
144
116
  })
145
117
  )
146
118
  .join('')
147
119
 
148
- const inner = `${chrome}${icon}${name}${badge}${catText}${catValue}${massLabel}${massValue}${sep}${statsSvg}`
120
+ const inner = `${chrome}${icon}${name}${badge}${metaSvg}${statsSvg}`
149
121
 
150
122
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${height}" viewBox="0 0 ${w} ${height}">${inner}</svg>`
151
123
  }
@@ -1,16 +1,31 @@
1
1
  import type {TextSpan} from '@shipload/sdk'
2
+ import {formatLocation, formatMassScaled} from '@shipload/sdk'
2
3
  import {panel} from '../primitives/panel.ts'
3
4
  import {iconHex} from '../primitives/icon-hex.ts'
4
- import {text} from '../primitives/text.ts'
5
- import {divider} from '../primitives/divider.ts'
5
+ import {entityIcon, entityIconSlugForName} from '../primitives/entity-icon.ts'
6
6
  import {moduleSlot} from '../primitives/module-slot.ts'
7
7
  import {quantityBadge} from '../primitives/quantity-badge.ts'
8
8
  import {wrapText} from '../primitives/wrap.ts'
9
9
  import {tokens} from '../tokens/index.ts'
10
+ import {
11
+ tierBorder,
12
+ metaRowBlock,
13
+ titleParts,
14
+ capabilityColor,
15
+ BADGE_Y,
16
+ HEADER_H,
17
+ ICON_Y,
18
+ BOTTOM_PAD,
19
+ } from './_shared.ts'
20
+
21
+ const HULL_MASS_LABELS = new Set(['mass', 'capacity'])
22
+
23
+ const ENTITY_COLOR = tokens.colors.brand.cyan
10
24
 
11
25
  export interface ShipPanelSlot {
12
26
  name?: string
13
27
  installed: boolean
28
+ capability?: string
14
29
  description?: string | TextSpan[]
15
30
  }
16
31
 
@@ -18,22 +33,35 @@ export interface ShipPanelProps {
18
33
  name: string
19
34
  tier: number
20
35
  quantity?: number
36
+ location?: {x: number; y: number}
21
37
  attributes: {capability: string; attributes: {label: string; value: number}[]}[]
22
38
  slots: ShipPanelSlot[]
23
39
  }
24
40
 
25
- function formatNumber(n: number): string {
26
- return n.toLocaleString('en-US')
27
- }
28
-
29
- function tierBorder(tier: number): string {
30
- return tokens.colors.tier[tier] ?? tokens.colors.surface.panelBorder
41
+ function formatHullValue(label: string, value: number): string {
42
+ return HULL_MASS_LABELS.has(label.toLowerCase())
43
+ ? formatMassScaled(value)
44
+ : value.toLocaleString('en-US')
31
45
  }
32
46
 
33
47
  const MODULE_LABEL_PREFIX = (capability: string) => `${capability}: `
34
48
 
35
- function rowHeightFor(slot: ShipPanelSlot): number {
36
- if (!slot.installed) return 24
49
+ function fallbackEntityCode(name: string): string {
50
+ const words = name
51
+ .replace(/\s+T\d+\s*$/i, '')
52
+ .trim()
53
+ .split(/\s+/)
54
+ .filter(Boolean)
55
+ if (words.length >= 2)
56
+ return words
57
+ .map((word) => word[0])
58
+ .join('')
59
+ .slice(0, 2)
60
+ .toUpperCase()
61
+ return (words[0] ?? 'EN').slice(0, 2).toUpperCase().padEnd(2, 'N')
62
+ }
63
+
64
+ function lineCountFor(slot: ShipPanelSlot): number {
37
65
  const desc = slot.description
38
66
  const plain =
39
67
  typeof desc === 'string'
@@ -41,12 +69,25 @@ function rowHeightFor(slot: ShipPanelSlot): number {
41
69
  : Array.isArray(desc)
42
70
  ? desc.map((s) => s.text).join('')
43
71
  : ''
44
- if (plain.length === 0) return 24
45
- const combined = MODULE_LABEL_PREFIX(slot.name ?? 'Module') + plain
46
- const lineCount = Math.max(1, wrapText({value: combined, charsPerLine: 36}).length)
72
+ if (plain.length === 0) return 0
73
+ const combined = MODULE_LABEL_PREFIX(slot.capability ?? slot.name ?? 'Module') + plain
74
+ return Math.max(1, wrapText({value: combined, charsPerLine: 36}).length)
75
+ }
76
+
77
+ function rowHeightFor(slot: ShipPanelSlot): number {
78
+ if (!slot.installed) return 24
79
+ const lineCount = lineCountFor(slot)
80
+ if (lineCount === 0) return 24
47
81
  return 10 + lineCount * 14
48
82
  }
49
83
 
84
+ function contentBottomOffsetFor(slot: ShipPanelSlot): number {
85
+ if (!slot.installed) return 14
86
+ const lineCount = lineCountFor(slot)
87
+ if (lineCount === 0) return 14
88
+ return 9 + (lineCount - 1) * 14 + 5
89
+ }
90
+
50
91
  export function renderShipPanel(props: ShipPanelProps): string {
51
92
  const w = tokens.spacing.panelWidth
52
93
  const pad = tokens.spacing.panelPadding
@@ -54,91 +95,64 @@ export function renderShipPanel(props: ShipPanelProps): string {
54
95
  const quantity = props.quantity ?? 0
55
96
 
56
97
  const hullGroup = props.attributes?.find((g) => g.capability.toLowerCase() === 'hull')
57
- const hullRows = hullGroup?.attributes ?? []
98
+ const hullAttrs = (hullGroup?.attributes ?? []).map((a) => ({
99
+ label: a.label,
100
+ value: formatHullValue(a.label, a.value),
101
+ }))
102
+ const metaRows = props.location
103
+ ? [{label: 'Location', value: formatLocation(props.location)}, ...hullAttrs]
104
+ : hullAttrs
58
105
 
59
- const headerH = 48
60
- const hullHeaderH = 20
61
- const hullRowH = 22
62
106
  const sectionGap = 12
63
- const rowHeights = props.slots.map(rowHeightFor)
64
- const modulesHeight = rowHeights.reduce((a, b) => a + b, 0)
65
- const height =
66
- headerH + hullHeaderH + hullRows.length * hullRowH + sectionGap + modulesHeight + pad
67
107
 
68
- const chrome = panel({width: w, height, borderColor: tierBorder(props.tier)})
69
-
70
- const icon = iconHex({
71
- x: pad,
72
- y: pad + 4,
73
- color: tokens.colors.text.accent,
74
- code: 'SH',
75
- })
108
+ const metaYStart = pad + HEADER_H
109
+ const {svg: metaSvg, height: metaH} = metaRowBlock(pad, metaYStart, innerW, metaRows)
76
110
 
77
- const name = text({
78
- x: pad + 34,
79
- y: pad + 22,
80
- value: props.name,
81
- size: tokens.typography.sizes.title,
82
- weight: 700,
83
- family: tokens.typography.display,
84
- })
85
-
86
- const badge = quantityBadge({x: w - pad, y: pad, quantity})
87
-
88
- const hullHeader = text({
89
- x: pad,
90
- y: pad + headerH,
91
- value: 'HULL',
92
- size: tokens.typography.sizes.subtitle,
93
- weight: 700,
94
- color: tokens.colors.text.secondary,
95
- letterSpacing: 1,
96
- })
111
+ const slotsYStart = metaYStart + metaH + sectionGap
97
112
 
98
- let y = pad + headerH + 6
99
- let hullSvg = ''
100
- for (const row of hullRows) {
101
- hullSvg +=
102
- text({
103
- x: pad,
104
- y: y + 12,
105
- value: row.label,
106
- size: tokens.typography.sizes.body,
107
- color: tokens.colors.text.secondary,
108
- }) +
109
- text({
110
- x: w - pad,
111
- y: y + 12,
112
- value: formatNumber(row.value),
113
- size: tokens.typography.sizes.body,
114
- weight: 700,
115
- anchor: 'end',
116
- }) +
117
- divider({
118
- x: pad,
119
- y: y + hullRowH - 4,
120
- width: innerW,
121
- color: tokens.colors.surface.panelBorderBright,
122
- })
123
- y += hullRowH
124
- }
125
-
126
- y += sectionGap
113
+ let y = slotsYStart
127
114
  let modulesSvg = ''
128
- for (let i = 0; i < props.slots.length; i++) {
129
- const slot = props.slots[i]!
115
+ let lastContentBottom = slotsYStart
116
+ props.slots.forEach((slot, i) => {
117
+ const accentColor = slot.installed
118
+ ? capabilityColor(slot.capability ?? slot.name ?? 'Module')
119
+ : ENTITY_COLOR
130
120
  modulesSvg += moduleSlot({
131
121
  x: pad,
132
122
  y,
133
123
  width: innerW,
134
124
  installed: slot.installed,
135
- capability: slot.name,
125
+ capability: slot.capability ?? slot.name,
136
126
  description: slot.description,
137
- accentColor: tokens.colors.brand.teal,
127
+ accentColor,
138
128
  })
139
- y += rowHeights[i]!
140
- }
129
+ lastContentBottom = y + contentBottomOffsetFor(slot)
130
+ if (i < props.slots.length - 1) y += rowHeightFor(slot)
131
+ })
132
+
133
+ const height = lastContentBottom + BOTTOM_PAD
134
+
135
+ const chrome = panel({width: w, height, borderColor: tierBorder(props.tier)})
136
+
137
+ const entitySlug = entityIconSlugForName(props.name)
138
+ const icon = entitySlug
139
+ ? entityIcon(entitySlug, {x: pad, y: pad + ICON_Y - 2, size: 28})
140
+ : iconHex({
141
+ x: pad,
142
+ y: pad + ICON_Y,
143
+ color: ENTITY_COLOR,
144
+ code: fallbackEntityCode(props.name),
145
+ })
146
+
147
+ const name = titleParts(pad + 34, pad + 22, props.name, props.tier)
148
+
149
+ const badge = quantityBadge({
150
+ x: w - pad,
151
+ y: pad + BADGE_Y,
152
+ quantity,
153
+ tone: ENTITY_COLOR,
154
+ })
141
155
 
142
- const inner = `${chrome}${icon}${name}${badge}${hullHeader}${hullSvg}${modulesSvg}`
156
+ const inner = `${chrome}${icon}${name}${badge}${metaSvg}${modulesSvg}`
143
157
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${height}" viewBox="0 0 ${w} ${height}">${inner}</svg>`
144
158
  }
@@ -1,4 +1,4 @@
1
- import {tierColors} from '@shipload/sdk'
1
+ import {categoryColors, tierColors} from '@shipload/sdk'
2
2
 
3
3
  export const colors = {
4
4
  surface: {
@@ -10,7 +10,7 @@ export const colors = {
10
10
  text: {
11
11
  primary: '#e6e8ec',
12
12
  secondary: '#8f98a8',
13
- muted: '#5b6373',
13
+ muted: '#7c8698',
14
14
  accent: '#f4c96b',
15
15
  },
16
16
  brand: {
@@ -18,16 +18,10 @@ export const colors = {
18
18
  teal: '#2fd6d1',
19
19
  cyan: '#6cb9ff',
20
20
  },
21
- category: {
22
- ore: '#C26D3F',
23
- crystal: '#4ADBFF',
24
- gas: '#B8E4A0',
25
- regolith: '#C4A57B',
26
- biomass: '#5A8B3E',
27
- },
21
+ category: categoryColors,
28
22
  tier: tierColors,
29
23
  accent: {
30
- component: '#8f98a8',
24
+ component: '#7E93C4',
31
25
  },
32
26
  capability: {
33
27
  engine: '#4a8abf',
@@ -9,6 +9,7 @@ export const ITEM_LOADER_T1 = 10103
9
9
  export const ITEM_MANUFACTURING_T1 = 10104
10
10
  export const ITEM_STORAGE_T1 = 10105
11
11
  export const ITEM_HAULER_T1 = 10106
12
+ export const ITEM_CONTAINER_T1_PACKED = 10200
12
13
  export const ITEM_SHIP_T1_PACKED = 10201
13
14
 
14
15
  export const MODULE_ENGINE = 1
@@ -49,7 +50,7 @@ export function cargoShipT1Packed(opts?: {
49
50
  })
50
51
  }
51
52
 
52
- export const ITEM_HULL_PLATES = 10001
53
+ export const ITEM_PLATE = 10001
53
54
 
54
55
  export const FIXTURES = {
55
56
  oreT1: cargoOreT1('0x123456789ABCDEF'),
@@ -61,8 +62,8 @@ export const FIXTURES = {
61
62
  stats: '0xDEADBEEF1234',
62
63
  modules: [],
63
64
  }),
64
- hullPlates: ServerContract.Types.cargo_item.from({
65
- item_id: ITEM_HULL_PLATES,
65
+ plate: ServerContract.Types.cargo_item.from({
66
+ item_id: ITEM_PLATE,
66
67
  quantity: 1,
67
68
  stats: '0x7FFF',
68
69
  modules: [],
@@ -115,6 +116,12 @@ export const FIXTURES = {
115
116
  stats: '0',
116
117
  modules: [],
117
118
  }),
119
+ containerT1: ServerContract.Types.cargo_item.from({
120
+ item_id: ITEM_CONTAINER_T1_PACKED,
121
+ quantity: 1,
122
+ stats: '0',
123
+ modules: [],
124
+ }),
118
125
  shipT1TwoModules: cargoShipT1Packed(),
119
126
  shipT1OnlyEngine: cargoShipT1Packed({onlyEngine: true}),
120
127
  } as const
@@ -1,110 +0,0 @@
1
- import type {CategoryIconShape} from '@shipload/sdk'
2
- import {el} from './svg.ts'
3
-
4
- export interface CategoryIconPathOpts {
5
- shape: CategoryIconShape
6
- cx: number
7
- cy: number
8
- size: number
9
- color: string
10
- strokeWidth?: number
11
- }
12
-
13
- export interface CategoryIconSvgOpts {
14
- size?: number
15
- color?: string
16
- strokeWidth?: number
17
- }
18
-
19
- function hexPoints(cx: number, cy: number, r: number): string {
20
- const pts: string[] = []
21
- for (let i = 0; i < 6; i++) {
22
- const angle = (Math.PI / 3) * i - Math.PI / 2
23
- pts.push(
24
- `${(cx + r * Math.cos(angle)).toFixed(2)},${(cy + r * Math.sin(angle)).toFixed(2)}`
25
- )
26
- }
27
- return pts.join(' ')
28
- }
29
-
30
- function diamondPoints(cx: number, cy: number, r: number): string {
31
- return [
32
- `${cx.toFixed(2)},${(cy - r).toFixed(2)}`,
33
- `${(cx + r).toFixed(2)},${cy.toFixed(2)}`,
34
- `${cx.toFixed(2)},${(cy + r).toFixed(2)}`,
35
- `${(cx - r).toFixed(2)},${cy.toFixed(2)}`,
36
- ].join(' ')
37
- }
38
-
39
- function starPoints(cx: number, cy: number, r: number): string {
40
- const inner = r * 0.45
41
- const pts: string[] = []
42
- for (let i = 0; i < 10; i++) {
43
- const angle = (Math.PI / 5) * i - Math.PI / 2
44
- const radius = i % 2 === 0 ? r : inner
45
- pts.push(
46
- `${(cx + radius * Math.cos(angle)).toFixed(2)},${(cy + radius * Math.sin(angle)).toFixed(2)}`
47
- )
48
- }
49
- return pts.join(' ')
50
- }
51
-
52
- export function categoryIconPath({
53
- shape,
54
- cx,
55
- cy,
56
- size,
57
- color,
58
- strokeWidth,
59
- }: CategoryIconPathOpts): string {
60
- const r = size / 2
61
- const stroked = strokeWidth && strokeWidth > 0
62
- const shapeAttrs = stroked
63
- ? {
64
- fill: 'none',
65
- stroke: color,
66
- 'stroke-width': strokeWidth,
67
- 'stroke-linejoin': 'round' as const,
68
- }
69
- : {fill: color}
70
- switch (shape) {
71
- case 'hex':
72
- return el('polygon', {points: hexPoints(cx, cy, r), ...shapeAttrs})
73
- case 'diamond':
74
- return el('polygon', {points: diamondPoints(cx, cy, r), ...shapeAttrs})
75
- case 'star':
76
- return el('polygon', {points: starPoints(cx, cy, r), ...shapeAttrs})
77
- case 'circle':
78
- return el('circle', {cx, cy, r, ...shapeAttrs})
79
- case 'square':
80
- return el('rect', {x: cx - r, y: cy - r, width: size, height: size, ...shapeAttrs})
81
- }
82
- const _exhaustive: never = shape
83
- throw new Error(`Unknown CategoryIconShape: ${String(_exhaustive)}`)
84
- }
85
-
86
- export function categoryIconSvg(shape: CategoryIconShape, opts: CategoryIconSvgOpts = {}): string {
87
- const size = opts.size ?? 16
88
- const color = opts.color ?? '#ffffff'
89
- const cx = size / 2
90
- const cy = size / 2
91
- const iconSize = size * 0.85
92
- const inner = categoryIconPath({
93
- shape,
94
- cx,
95
- cy,
96
- size: iconSize,
97
- color,
98
- strokeWidth: opts.strokeWidth,
99
- })
100
- return el(
101
- 'svg',
102
- {
103
- xmlns: 'http://www.w3.org/2000/svg',
104
- width: size,
105
- height: size,
106
- viewBox: `0 0 ${size} ${size}`,
107
- },
108
- inner
109
- )
110
- }
@@ -1,38 +0,0 @@
1
- import {text} from './text.ts'
2
- import {tokens} from '../tokens/index.ts'
3
-
4
- export interface CompactRowProps {
5
- x: number
6
- y: number
7
- width: number
8
- label: string
9
- value: string
10
- labelColor?: string
11
- valueColor?: string
12
- }
13
-
14
- export function compactRow(p: CompactRowProps): string {
15
- const labelColor = p.labelColor ?? tokens.colors.text.secondary
16
- const valueColor = p.valueColor ?? tokens.colors.text.primary
17
- return (
18
- text({
19
- x: p.x,
20
- y: p.y,
21
- value: p.label,
22
- size: 11,
23
- weight: 500,
24
- family: tokens.typography.sans,
25
- color: labelColor,
26
- }) +
27
- text({
28
- x: p.x + p.width,
29
- y: p.y,
30
- value: p.value,
31
- size: 11,
32
- weight: 700,
33
- family: tokens.typography.sans,
34
- color: valueColor,
35
- anchor: 'end',
36
- })
37
- )
38
- }