astryx-dracula 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/BRAND.md +30 -18
  2. package/README.md +17 -12
  3. package/USAGE.md +29 -10
  4. package/astryx-dracula.js +19 -13
  5. package/astryx-dracula.variants.d.ts +15 -0
  6. package/package.json +59 -17
  7. package/shared/auth-chrome-config.ts +18 -0
  8. package/shared/auth-chrome.tsx +25 -0
  9. package/shared/auth-copy.ts +22 -0
  10. package/shared/chaptered-doc-config.ts +92 -0
  11. package/shared/chaptered-doc.tsx +332 -0
  12. package/shared/chart-hues.ts +27 -0
  13. package/shared/chart-labels.tsx +44 -0
  14. package/shared/chart-legend.tsx +100 -0
  15. package/shared/chart-panel-style.ts +13 -0
  16. package/shared/data-bar.tsx +160 -0
  17. package/shared/gallery-image.ts +20 -0
  18. package/shared/login-demo.ts +7 -0
  19. package/shared/metric-delta.tsx +36 -0
  20. package/shared/order-desk.tsx +273 -0
  21. package/shared/revenue-chart.tsx +174 -0
  22. package/shared/scene-castle.tsx +322 -0
  23. package/shared/scene-frame.tsx +88 -0
  24. package/shared/scene-hues.ts +49 -0
  25. package/shared/scene-tile.tsx +128 -0
  26. package/shared/settings-data.ts +125 -0
  27. package/shared/settings-rows.tsx +179 -0
  28. package/shared/sparkline.tsx +94 -0
  29. package/shared/sso-icons.tsx +30 -0
  30. package/templates/ai-chat-landing.tsx +66 -33
  31. package/templates/ai-chat.tsx +36 -19
  32. package/templates/blank.tsx +4 -3
  33. package/templates/centered-hero.tsx +14 -125
  34. package/templates/classic-gallery.tsx +20 -66
  35. package/templates/contact-form.tsx +59 -39
  36. package/templates/dashboard-portfolio.tsx +143 -226
  37. package/templates/dashboard.tsx +122 -122
  38. package/templates/detail-page.tsx +95 -91
  39. package/templates/documentation-design.template.mjs +1 -1
  40. package/templates/documentation-design.tsx +83 -96
  41. package/templates/documentation-technical.template.mjs +1 -1
  42. package/templates/documentation-technical.tsx +29 -19
  43. package/templates/documentation.template.mjs +1 -1
  44. package/templates/documentation.tsx +52 -74
  45. package/templates/editor.tsx +112 -84
  46. package/templates/file-explorer.tsx +131 -84
  47. package/templates/form-two-column.tsx +61 -125
  48. package/templates/gallery-hero.tsx +15 -171
  49. package/templates/ide.tsx +22 -20
  50. package/templates/incident-console.tsx +104 -70
  51. package/templates/kanban-board.tsx +99 -42
  52. package/templates/library.template.mjs +2 -2
  53. package/templates/library.tsx +37 -42
  54. package/templates/login-card.tsx +58 -87
  55. package/templates/login-split.tsx +88 -126
  56. package/templates/login-sso.tsx +105 -64
  57. package/templates/login.tsx +107 -53
  58. package/templates/messaging-shell.tsx +171 -144
  59. package/templates/mixed-gallery.tsx +29 -130
  60. package/templates/payment-form.tsx +368 -290
  61. package/templates/product-detail.tsx +39 -58
  62. package/templates/product-gallery.tsx +30 -41
  63. package/templates/product-tour.template.mjs +6 -0
  64. package/templates/product-tour.tsx +916 -0
  65. package/templates/settings-dialog.tsx +135 -282
  66. package/templates/settings-sidebar.tsx +178 -268
  67. package/templates/settings.tsx +41 -14
  68. package/templates/shell-nav.tsx +31 -24
  69. package/templates/shell-side-nav.tsx +29 -20
  70. package/templates/shell-top-nav.tsx +80 -44
  71. package/templates/side-gallery.tsx +38 -94
  72. package/templates/table-grouped.tsx +62 -34
  73. package/templates/table-page-chart.tsx +23 -302
  74. package/templates/table-page-heatmap-status.tsx +156 -92
  75. package/templates/table-page-shoe-store-heatmap.tsx +18 -293
  76. package/templates/table-page.tsx +23 -7
  77. package/templates/table.tsx +23 -8
  78. package/templates/tech-report.template.mjs +6 -0
  79. package/templates/tech-report.tsx +1486 -0
  80. package/templates/theme-showcase.template.mjs +1 -1
  81. package/templates/theme-showcase.tsx +299 -237
  82. package/theme.css +76 -14
  83. package/tokens.css +425 -30
@@ -1,6 +1,6 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
  // XLE (canonical structure, validated with `bunx astryx layout check`):
3
- // L > LC[p=6] > G[c={min:360} g=8] > (V[g=4] > AR + (G[c=3 g=3] > C*6)) + (V[g=6] > (V[g=2] > Hd"Midnight Ceremony Mug & Plate Set"[level=1 t=display-2] + (H[g=2] > Tx"$89.00"[t=large] + Tx"$119.00"[t=body] + Bd.pink"Sale")) + Tx"A hand-thrown mug and plate set"[t=large] + (V[g=2] > Tx"Glaze"[t=label] + SG) + (V[g=2] > Tx"Finish"[t=label] + SG) + (V[g=2] > Tx"Quantity"[t=label] + (H[g=1] > B.ghost"-" + TI"1" + B.ghost"+")) + (V[g=2] > B.primary"Add to Cart" + B.secondary"Buy it now") + (ColG > Col"Composition" + Col"Delivery & Returns" + Col"Dimensions"))
3
+ // L > LC[p=6] > G[c={min:280} g=8] > (V[g=3] > AR[ratio=4/5] + (G[c=3 g=2] > (AR[ratio=1] > SelectableCard)*6)) + (V[g=8] > (V[g=3] > Hd"Midnight Ceremony Mug & Plate Set"[level=1 t=display-2] + (H[g=1] > Ic*5 + Tx"4.3 (128)") + (H[g=2] > Tx"$89.00"[t=large] + Tx"$119.00"[t=body] + Tk"Sale")) + Tx"A hand-thrown mug and plate set"[t=large] + (V[g=3] > Tx"Glaze"[t=label] + SG) + (V[g=3] > Tx"Finish"[t=label] + SG) + (V[g=3] > Tx"Quantity"[t=label] + (H[g=1] > B.ghost"-" + TI"1" + B.ghost"+")) + (V[g=3] > B.primary"Add to cart" + B.secondary"Buy it now") + (ColG > Col"Composition" + Col"Delivery & Returns" + Col"Dimensions"))
4
4
 
5
5
  import {useState} from 'react';
6
6
  import {VStack, HStack, Layout, LayoutContent} from '@astryxdesign/core/Layout';
@@ -14,16 +14,18 @@ import {
14
14
  SegmentedControl,
15
15
  SegmentedControlItem,
16
16
  } from '@astryxdesign/core/SegmentedControl';
17
- import {Badge} from '@astryxdesign/core/Badge';
17
+ import {Token} from '@astryxdesign/core/Token';
18
18
  import {Banner} from '@astryxdesign/core/Banner';
19
19
  import {Divider} from '@astryxdesign/core/Divider';
20
20
  import {Collapsible, CollapsibleGroup} from '@astryxdesign/core/Collapsible';
21
21
  import {AspectRatio} from '@astryxdesign/core/AspectRatio';
22
22
  import {SelectableCard} from '@astryxdesign/core/SelectableCard';
23
23
  import type {CSSProperties} from 'react';
24
+ import {galleryImageClip} from 'astryx-dracula/shared/gallery-image';
25
+ import {SceneTile} from 'astryx-dracula/shared/scene-tile';
24
26
 
25
27
  // Custom CSS here is limited to what Astryx components can't express today:
26
- // - image fill + corner radius (no Image primitive — #2582)
28
+ // - image radius-clip (no Image primitive — #2582, see shared/gallery-image)
27
29
  // - the sticky info column (no sticky prop on Astryx layout primitives — #2613)
28
30
  // Keeps the info column in view while the gallery scrolls. No sticky prop on
29
31
  // Astryx layout primitives.
@@ -32,49 +34,16 @@ const stickyInfo: CSSProperties = {
32
34
  top: 'var(--spacing-8)',
33
35
  alignSelf: 'start',
34
36
  };
35
- // Product scenes are inline Dracula SVG — there is no Image primitive in
36
- // Astryx (#2582). Fills the AspectRatio box + rounds hero corners. No
37
- // objectFit/radius props on AspectRatio.
38
- const heroImage: CSSProperties = {
39
- borderRadius: 'var(--radius-container)',
40
- overflow: 'clip',
41
- };
42
- // Fills the thumbnail card. Corner radius + selection ring come from
43
- // SelectableCard; the scene only needs to fill the box (#2582).
44
- const thumbImage: CSSProperties = {
45
- width: '100%',
46
- height: '100%',
47
- display: 'block',
48
- };
49
37
 
50
38
  // One Dracula placeholder scene per view, drawn from the fixed badge
51
- // vocabulary. Resolves to theme tokens so the gallery stays on-brand in the
52
- // dark-only theme.
53
- function ProductScene({hue, label}: {hue: string; label: string}) {
54
- return (
55
- <svg
56
- viewBox="0 0 400 300"
57
- preserveAspectRatio="xMidYMid slice"
58
- style={thumbImage}
59
- role="img"
60
- aria-label={label}>
61
- <rect width="400" height="300" fill="var(--dracula-bg-light)" />
62
- <g
63
- transform="translate(200 150)"
64
- fill="none"
65
- stroke="var(--dracula-comment)"
66
- strokeWidth="5"
67
- strokeLinecap="round"
68
- strokeLinejoin="round">
69
- <rect x="-44" y="-44" width="88" height="88" rx="5" />
70
- <circle cx="18" cy="-18" r="2.5" fill={hue} stroke="none" />
71
- <path d="M-34 30 L-8 0 L10 18 L20 8 L34 24" />
72
- </g>
73
- </svg>
74
- );
39
+ // vocabulary (shared/scene-tile large fork). Resolves to theme tokens so the
40
+ // gallery stays on-brand in the dark-only theme.
41
+ function ProductScene({hue, label}: {hue: SceneHue; label: string}) {
42
+ return <SceneTile label={label} hue={hue} size="lg" />;
75
43
  }
76
44
 
77
45
  import {Minus, Plus, Star} from 'lucide-react';
46
+ import type {SceneHue} from 'astryx-dracula/shared/scene-hues';
78
47
 
79
48
  // ─── Star Rating ─────────────────────────────────────────────────────────────
80
49
  function StarRating({rating, count}: {rating: number; count: number}) {
@@ -84,10 +53,15 @@ function StarRating({rating, count}: {rating: number; count: number}) {
84
53
  return (
85
54
  <HStack gap={1} vAlign="center">
86
55
  {Array.from({length: filled}, (_, i) => (
87
- <Icon key={`full-${i}`} icon={Star} size="sm" color="warning" />
56
+ <Icon key={`full-${i}`} icon={Star} size="sm" color="yellow" />
88
57
  ))}
58
+ {/* secondary, not disabled: --color-icon-disabled #6272A4 is 2.51:1 on
59
+ the card #343746, under the 3:1 non-text floor, and reads as "this
60
+ control is unavailable" — but a star is a data mark, not a control.
61
+ --color-icon-secondary #9AA1BC is 4.60:1, one step under the filled
62
+ yellow (7.4:1), so the two halves still read as one rating. */}
89
63
  {Array.from({length: empty}, (_, i) => (
90
- <Icon key={`empty-${i}`} icon={Star} size="sm" color="disabled" />
64
+ <Icon key={`empty-${i}`} icon={Star} size="sm" color="secondary" />
91
65
  ))}
92
66
  <Text type="body" color="secondary" hasTabularNumbers>
93
67
  {rating} ({count})
@@ -100,13 +74,19 @@ function StarRating({rating, count}: {rating: number; count: number}) {
100
74
  // IMAGES[selected] is the hero; all six double as thumbnails so the 3-column
101
75
  // grid closes into two full rows (a slice(1) subset left a ragged 3+2 row and
102
76
  // a fallback entry that never rendered).
103
- const IMAGES = [
104
- 'var(--dracula-purple)',
77
+ const IMAGES: readonly SceneHue[] = [
105
78
  'var(--dracula-cyan)',
106
79
  'var(--dracula-pink)',
107
80
  'var(--dracula-yellow)',
108
81
  'var(--dracula-green)',
109
82
  'var(--dracula-orange)',
83
+ // The sixth is `muted` (Comment): a legal / neutral / out-of-stock
84
+ // register, which is exactly what a product photo set needs an extra slot
85
+ // for. Cyan, pink, yellow, green and orange are all saturated identity
86
+ // colours; a sixth saturated one would read as a sixth product variant
87
+ // rather than as "this one is the plain shot". Keeps the 3-column grid
88
+ // closing on two full rows, which is the reason the set exists.
89
+ 'var(--dracula-comment)',
110
90
  ];
111
91
 
112
92
  // ─── Product Data ───────────────────────────────────────────────────────────
@@ -146,17 +126,17 @@ function ImageGallery({
146
126
  selected: number;
147
127
  onSelect: (i: number) => void;
148
128
  }) {
149
- const heroHue = IMAGES[selected];
129
+ const heroHue: SceneHue = IMAGES[selected];
150
130
  const thumbnails = IMAGES;
151
131
 
152
132
  return (
153
133
  <VStack gap={3}>
154
- <AspectRatio ratio={4 / 5} style={heroImage}>
134
+ <AspectRatio ratio={4 / 5} style={galleryImageClip}>
155
135
  <ProductScene hue={heroHue} label={PRODUCT.name} />
156
136
  </AspectRatio>
157
137
  <Grid columns={3} gap={2}>
158
138
  {thumbnails.map((hue, i) => (
159
- <AspectRatio key={i} ratio={1}>
139
+ <AspectRatio key={hue} ratio={1}>
160
140
  <SelectableCard
161
141
  label={`Product image ${i + 1}`}
162
142
  isSelected={selected === i}
@@ -188,26 +168,26 @@ function ProductInfo() {
188
168
  const increment = () => setQuantity(q => Math.min(10, (q ?? 1) + 1));
189
169
 
190
170
  return (
191
- <VStack gap={5}>
192
- <VStack gap={2}>
171
+ <VStack gap={8}>
172
+ <VStack gap={3}>
193
173
  <Heading level={1} type="display-2">
194
174
  {PRODUCT.name}
195
175
  </Heading>
196
176
  <StarRating rating={4.3} count={128} />
197
177
  <HStack gap={2} vAlign="center">
198
- <Text type="large" weight="bold" hasTabularNumbers>
178
+ <Text type="large" hasTabularNumbers>
199
179
  {fmt(PRODUCT.price)}
200
180
  </Text>
201
181
  <Text type="body" color="secondary" hasStrikethrough hasTabularNumbers>
202
182
  {fmt(PRODUCT.originalPrice)}
203
183
  </Text>
204
- <Badge variant="pink" label="Sale" />
184
+ <Token label="Sale" color="yellow" />
205
185
  </HStack>
206
186
  </VStack>
207
187
  <Text type="large" weight="normal">
208
188
  {PRODUCT.description}
209
189
  </Text>
210
- <VStack gap={2}>
190
+ <VStack gap={3}>
211
191
  <Text type="label">Glaze</Text>
212
192
  <VStack hAlign="start">
213
193
  <SegmentedControl value={color} onChange={setColor} label="Glaze">
@@ -221,7 +201,7 @@ function ProductInfo() {
221
201
  </SegmentedControl>
222
202
  </VStack>
223
203
  </VStack>
224
- <VStack gap={2}>
204
+ <VStack gap={3}>
225
205
  <Text type="label">Finish</Text>
226
206
  <VStack hAlign="start">
227
207
  <SegmentedControl value={finish} onChange={setFinish} label="Finish">
@@ -235,7 +215,7 @@ function ProductInfo() {
235
215
  </SegmentedControl>
236
216
  </VStack>
237
217
  </VStack>
238
- <VStack gap={2}>
218
+ <VStack gap={3}>
239
219
  <Text type="label">Quantity</Text>
240
220
  <HStack gap={1} vAlign="center">
241
221
  <Button
@@ -269,7 +249,7 @@ function ProductInfo() {
269
249
  />
270
250
  </HStack>
271
251
  </VStack>
272
- <VStack gap={2}>
252
+ <VStack gap={3}>
273
253
  {notice && (
274
254
  <Banner
275
255
  status={notice.status}
@@ -280,7 +260,7 @@ function ProductInfo() {
280
260
  />
281
261
  )}
282
262
  <Button
283
- label="Add to Cart"
263
+ label="Add to cart"
284
264
  variant="primary"
285
265
  size="lg"
286
266
  clickAction={() =>
@@ -292,6 +272,7 @@ function ProductInfo() {
292
272
  />
293
273
  <Button
294
274
  label="Buy it now"
275
+ variant="secondary"
295
276
  size="lg"
296
277
  clickAction={() =>
297
278
  setNotice({
@@ -352,7 +333,7 @@ export default function ProductDetail() {
352
333
  contentWidth={1200}
353
334
  content={
354
335
  <LayoutContent padding={6}>
355
- <Grid columns={{minWidth: 320, repeat: 'fit'}} gap={5}>
336
+ <Grid columns={{minWidth: 280, repeat: 'fit'}} gap={8}>
356
337
  <ImageGallery
357
338
  selected={selectedThumb}
358
339
  onSelect={setSelectedThumb}
@@ -1,6 +1,6 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
  // XLE (canonical structure, validated with `bunx astryx layout check`):
3
- // L > LC > V[g=6] > (G[c={min:280} g=4 a=start] > Hd"Small comforts for the midnight hours."[level=1] + (V[g=3 a=start] > Tx"Provisions from the castle workshops"[t=body] + B.primary"Browse the collection")) + (G[c={min:300} g=6] > (V[g=3] > (C[p=0] > AR[ratio=1]) + (V[g=1] > Hd"Nightfall Stoneware Mug"[level=3] + Tx"Description"[t=body color=secondary] + Tx.lg"$75.00"))*6)
3
+ // L > LC[p=6] > V[g=8] > (G[c={min:280} g=4 a=start] > Hd"Small comforts for the midnight hours."[level=1 t=display-2] + (V[g=3 a=start] > Tx"Provisions from the castle workshops"[t=body] + B.primary"Browse the collection")) + (V[g=8] > (V[g=2] > Hd"The collection"[level=2] + Tx"Each piece is made in small batches"[t=body]) + (G[c={min:280} g=8] > (V[g=3] > (C[p=0] > AR[ratio=1]) + (V[g=1] > Hd"Nightfall Stoneware Mug"[level=3] + Tx"A hand-thrown mug that sits easy"[t=body color=secondary] + Tx.lg"$75.00"))*6))
4
4
 
5
5
  import {VStack, Layout, LayoutContent} from '@astryxdesign/core/Layout';
6
6
  import {Text, Heading} from '@astryxdesign/core/Text';
@@ -10,17 +10,12 @@ import {AspectRatio} from '@astryxdesign/core/AspectRatio';
10
10
  import {Card} from '@astryxdesign/core/Card';
11
11
  import {Icon} from '@astryxdesign/core/Icon';
12
12
  import {ArrowRight} from 'lucide-react';
13
- import type {CSSProperties} from 'react';
13
+ import {SceneTile} from 'astryx-dracula/shared/scene-tile';
14
+ import type {SceneHue} from 'astryx-dracula/shared/scene-hues';
14
15
 
15
16
  // ─── Styles ─────────────────────────────────────────────────────────────────
16
- // Product tiles are inline Dracula SVG — there is no Image primitive to fill
17
- // the AspectRatio box with `object-fit` (#2582).
18
-
19
- const svgImage: CSSProperties = {
20
- width: '100%',
21
- height: '100%',
22
- display: 'block',
23
- };
17
+ // Product tile art lives in shared/scene-tile (large fork); there is no Image
18
+ // primitive to fill the AspectRatio box with `object-fit` (#2582).
24
19
 
25
20
  // ─── Product Data ───────────────────────────────────────────────────────────
26
21
 
@@ -29,7 +24,7 @@ interface Product {
29
24
  name: string;
30
25
  description: string;
31
26
  price: number;
32
- hue: string;
27
+ hue: SceneHue;
33
28
  }
34
29
 
35
30
  // One accent per product from the fixed badge vocabulary.
@@ -40,7 +35,7 @@ const PRODUCTS: Product[] = [
40
35
  description:
41
36
  'A hand-thrown mug that sits easy in the hand through the longest night watch.',
42
37
  price: 75.0,
43
- hue: 'var(--dracula-purple)',
38
+ hue: 'var(--dracula-orange)',
44
39
  },
45
40
  {
46
41
  id: 2,
@@ -93,25 +88,11 @@ function ProductCard({product}: {product: Product}) {
93
88
  <VStack gap={3}>
94
89
  <Card padding={0}>
95
90
  <AspectRatio ratio={1}>
96
- <svg
97
- viewBox="0 0 400 300"
98
- preserveAspectRatio="xMidYMid slice"
99
- style={svgImage}
100
- role="img"
101
- aria-label={`${product.name} thumbnail`}>
102
- <rect width="400" height="300" fill="var(--dracula-bg-light)" />
103
- <g
104
- transform="translate(200 150)"
105
- fill="none"
106
- stroke="var(--dracula-comment)"
107
- strokeWidth="5"
108
- strokeLinecap="round"
109
- strokeLinejoin="round">
110
- <rect x="-44" y="-44" width="88" height="88" rx="5" />
111
- <circle cx="18" cy="-18" r="2.5" fill={product.hue} stroke="none" />
112
- <path d="M-34 30 L-8 0 L10 18 L20 8 L34 24" />
113
- </g>
114
- </svg>
91
+ <SceneTile
92
+ label={`${product.name} thumbnail`}
93
+ hue={product.hue}
94
+ size="lg"
95
+ />
115
96
  </AspectRatio>
116
97
  </Card>
117
98
  <VStack gap={1}>
@@ -119,7 +100,7 @@ function ProductCard({product}: {product: Product}) {
119
100
  <Text type="body" color="secondary" maxLines={2}>
120
101
  {product.description}
121
102
  </Text>
122
- <Text type="large" weight="bold" hasTabularNumbers>
103
+ <Text type="large" hasTabularNumbers>
123
104
  {fmt(product.price)}
124
105
  </Text>
125
106
  </VStack>
@@ -136,17 +117,16 @@ export default function ProductGallery() {
136
117
  contentWidth={1200}
137
118
  content={
138
119
  <LayoutContent padding={6}>
139
- <VStack gap={6}>
120
+ <VStack gap={8}>
140
121
  {/* Header — Grid handles responsive stacking */}
141
122
  <Grid columns={{minWidth: 280}} gap={4} align="start">
142
- <Heading level={1}>
123
+ <Heading level={1} type="display-2">
143
124
  Small comforts for the midnight hours.
144
125
  </Heading>
145
126
  <VStack gap={3} hAlign="start">
146
127
  <Text type="body">
147
128
  Provisions from the castle workshops: kiln-fired, hand-poured,
148
- and stitched by the coven. Each piece is made in small batches
149
- and restocked with every full moon.
129
+ and stitched by the coven.
150
130
  </Text>
151
131
  <Button
152
132
  label="Browse the collection"
@@ -162,11 +142,20 @@ export default function ProductGallery() {
162
142
  </Grid>
163
143
 
164
144
  {/* Product Grid — reflows 3 → 2 → 1 columns as width narrows */}
165
- <Grid columns={{minWidth: 300}} gap={6} id="products">
166
- {PRODUCTS.map(product => (
167
- <ProductCard key={product.id} product={product} />
168
- ))}
169
- </Grid>
145
+ <VStack gap={8}>
146
+ <VStack gap={2}>
147
+ <Heading level={2}>The collection</Heading>
148
+ <Text type="body" color="secondary">
149
+ Each piece is made in small batches and restocked with every
150
+ full moon.
151
+ </Text>
152
+ </VStack>
153
+ <Grid columns={{minWidth: 280}} gap={4} id="products">
154
+ {PRODUCTS.map(product => (
155
+ <ProductCard key={product.id} product={product} />
156
+ ))}
157
+ </Grid>
158
+ </VStack>
170
159
  </VStack>
171
160
  </LayoutContent>
172
161
  }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ type: 'page',
3
+ name: 'Product Tour',
4
+ description: 'Chapter rail, walkthrough content, and outline',
5
+ category: 'marketing',
6
+ };