coherent-docs-theme 1.0.6 → 1.0.7

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.
Binary file
Binary file
Binary file
@@ -31,9 +31,13 @@ let {
31
31
  } = Astro.props;
32
32
 
33
33
  let slides: CustomSlide[] = [];
34
+ const baseUrl =
35
+ import.meta.env.BASE_URL === "/" ? "" : import.meta.env.BASE_URL;
34
36
 
35
37
  if (dir) {
36
- const publicPath = path.join(process.cwd(), "public", dir);
38
+ const cleanDir = dir.replace(/\/+$/, "");
39
+ const publicPath = path.join(process.cwd(), "public", cleanDir);
40
+
37
41
  try {
38
42
  if (fs.existsSync(publicPath)) {
39
43
  const files = fs.readdirSync(publicPath);
@@ -41,11 +45,16 @@ if (dir) {
41
45
  /\.(png|jpe?g|gif|svg|webp)$/i.test(file),
42
46
  );
43
47
  slides.push(
44
- ...imageFiles.map((file) => ({ src: `/${dir}/${file}` }))
48
+ ...imageFiles.map((file) => ({
49
+ src: `${baseUrl}/${cleanDir}/${file}`,
50
+ })),
45
51
  );
46
52
  }
47
53
  } catch (e) {
48
- console.error(`[Gallery] Error while reading the dir: ${publicPath}`, e);
54
+ console.error(
55
+ `[Gallery] Error while reading the dir: ${publicPath}`,
56
+ e,
57
+ );
49
58
  }
50
59
  }
51
60
 
@@ -78,13 +87,26 @@ if (slides.length === 0) {
78
87
  const altText = slide.alt || "Gallery slide";
79
88
 
80
89
  const imgContent = isLocal ? (
81
- <Image src={slide.src as ImageMetadata} alt={altText} class="slide-img" />
90
+ <Image
91
+ src={slide.src as ImageMetadata}
92
+ alt={altText}
93
+ class="slide-img"
94
+ />
82
95
  ) : (
83
- <img src={slide.src as string} alt={altText} class="slide-img" />
96
+ <img
97
+ src={slide.src as string}
98
+ alt={altText}
99
+ class="slide-img"
100
+ />
84
101
  );
85
102
 
86
103
  const slideBody = slide.link ? (
87
- <a href={slide.link} target="_blank" rel="noopener noreferrer" class="slide-link">
104
+ <a
105
+ href={slide.link}
106
+ target="_blank"
107
+ rel="noopener noreferrer"
108
+ class="slide-link"
109
+ >
88
110
  {imgContent}
89
111
  </a>
90
112
  ) : (
@@ -94,7 +116,7 @@ if (slides.length === 0) {
94
116
  return (
95
117
  <li class="slide">
96
118
  {slideBody}
97
-
119
+
98
120
  {slide.description && (
99
121
  <div class="slide-caption">
100
122
  <p>{slide.description}</p>
@@ -171,7 +193,7 @@ if (slides.length === 0) {
171
193
  padding: 1rem 1.5rem;
172
194
  text-align: center;
173
195
  box-sizing: border-box;
174
- pointer-events: none;
196
+ pointer-events: none;
175
197
  }
176
198
 
177
199
  .slide-caption p {
@@ -202,8 +224,12 @@ if (slides.length === 0) {
202
224
  background: rgba(0, 0, 0, 0.9);
203
225
  }
204
226
 
205
- .nav.left { left: 1rem; }
206
- .nav.right { right: 1rem; }
227
+ .nav.left {
228
+ left: 1rem;
229
+ }
230
+ .nav.right {
231
+ right: 1rem;
232
+ }
207
233
  </style>
208
234
 
209
235
  <script>
@@ -225,15 +251,21 @@ if (slides.length === 0) {
225
251
 
226
252
  if (this.totalSlides === 0) return;
227
253
 
228
- this.querySelector(".left")?.addEventListener("click", () => this.move("back"));
229
- this.querySelector(".right")?.addEventListener("click", () => this.move("forward"));
254
+ this.querySelector(".left")?.addEventListener("click", () =>
255
+ this.move("back"),
256
+ );
257
+ this.querySelector(".right")?.addEventListener("click", () =>
258
+ this.move("forward"),
259
+ );
230
260
 
231
261
  this.resetAutoSlide();
232
262
  }
233
263
 
234
264
  move(direction: "back" | "forward") {
235
265
  if (direction === "back") {
236
- this.currentSlide = (this.currentSlide - 1 + this.totalSlides) % this.totalSlides;
266
+ this.currentSlide =
267
+ (this.currentSlide - 1 + this.totalSlides) %
268
+ this.totalSlides;
237
269
  } else {
238
270
  this.currentSlide = (this.currentSlide + 1) % this.totalSlides;
239
271
  }
@@ -249,7 +281,10 @@ if (slides.length === 0) {
249
281
  resetAutoSlide() {
250
282
  if (this.autoSlide > 0) {
251
283
  if (this.interval) clearInterval(this.interval);
252
- this.interval = setInterval(() => this.move("forward"), this.autoSlide) as any;
284
+ this.interval = setInterval(
285
+ () => this.move("forward"),
286
+ this.autoSlide,
287
+ ) as any;
253
288
  }
254
289
  }
255
290
  }
@@ -257,4 +292,4 @@ if (slides.length === 0) {
257
292
  if (!customElements.get("gallery-slider")) {
258
293
  customElements.define("gallery-slider", GallerySlider);
259
294
  }
260
- </script>
295
+ </script>
@@ -0,0 +1,7 @@
1
+ ---
2
+ import DefaultSidebar from "@astrojs/starlight/components/Sidebar.astro";
3
+ import TopicSwitcher from "../internal-components/TopicSwitcher.astro";
4
+ ---
5
+
6
+ <TopicSwitcher />
7
+ <DefaultSidebar {...Astro.props} />
@@ -0,0 +1,30 @@
1
+ ---
2
+ export interface Props {
3
+ light?: string;
4
+ dark?: string;
5
+ }
6
+
7
+ const {
8
+ light = '#8e6400',
9
+ dark = '#F5BC35'
10
+ } = Astro.props;
11
+ ---
12
+
13
+ <span class="highlight" style={`--light-color: ${light}; --dark-color: ${dark};`}>
14
+ <slot />
15
+ </span>
16
+
17
+ <style>
18
+ .highlight {
19
+ font-weight: 600;
20
+ color: var(--light-color);
21
+ }
22
+
23
+ :global([data-theme='dark']) .highlight {
24
+ color: var(--dark-color);
25
+ }
26
+
27
+ :global([data-theme='light']) .highlight {
28
+ color: var(--light-color);
29
+ }
30
+ </style>
@@ -0,0 +1,19 @@
1
+ ---
2
+ export interface Props {
3
+ href: string;
4
+ blank?: boolean;
5
+ }
6
+
7
+ const {
8
+ href,
9
+ blank = true
10
+ } = Astro.props;
11
+ ---
12
+
13
+ <a
14
+ href={href}
15
+ target={blank ? "_blank" : undefined}
16
+ rel={blank ? "noopener noreferrer" : undefined}
17
+ >
18
+ <slot />
19
+ </a>
@@ -0,0 +1,63 @@
1
+ ---
2
+ import { Card, CardGrid } from '@astrojs/starlight/components';
3
+
4
+ export interface Props {
5
+ cards: {
6
+ href: string;
7
+ title: string;
8
+ imgSrc: string;
9
+ description: string;
10
+ }[];
11
+ }
12
+
13
+ const { cards } = Astro.props;
14
+ ---
15
+
16
+ <CardGrid class="custom-card-grid">
17
+ {cards.map((card) => (
18
+ <a class="card-link" href={card.href}>
19
+ <Card title={card.title}>
20
+ <img src={card.imgSrc} class="card-image" alt={card.title} />
21
+ <p>{card.description}</p>
22
+ </Card>
23
+ </a>
24
+ ))}
25
+ </CardGrid>
26
+
27
+ <style>
28
+ .custom-card-grid {
29
+ display: grid;
30
+ grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
31
+ gap: 1.5rem;
32
+ margin-top: 3rem;
33
+ }
34
+
35
+ @media (max-width: 90rem) {
36
+ .custom-card-grid {
37
+ grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
38
+ }
39
+ }
40
+
41
+ @media (max-width: 70rem) {
42
+ .custom-card-grid {
43
+ grid-template-columns: 1fr !important;
44
+ }
45
+ }
46
+
47
+ .card-link {
48
+ text-decoration: none;
49
+ position: relative;
50
+ }
51
+
52
+ .card-link p {
53
+ color: var(--sl-color-text);
54
+ }
55
+
56
+ .card-image {
57
+ width: 100%;
58
+ height: 10rem;
59
+ object-fit: cover;
60
+ object-position: top center;
61
+ margin-bottom: 1rem;
62
+ }
63
+ </style>
@@ -0,0 +1,24 @@
1
+ ---
2
+ export interface Props {
3
+ href: string;
4
+ product?: string;
5
+ }
6
+
7
+ const { href, product } = Astro.props;
8
+
9
+ const currentProduct = product || process.env.DOCS_PRODUCT || 'gameface';
10
+
11
+ const productPaths: Record<string, string> = {
12
+ 'gameface': 'cpp-gameface',
13
+ 'prysm': 'cpp-prysm',
14
+ };
15
+
16
+ const urlSegment = productPaths[currentProduct.toLowerCase()] || `cpp-${currentProduct.toLowerCase()}`;
17
+
18
+ const baseUrl = `https://docs.coherent-labs.com/${urlSegment}`;
19
+
20
+ const cleanHref = !href ? "/" : href.startsWith("/") ? href : `/${href}`;
21
+ const fullUrl = `${baseUrl}${cleanHref}`;
22
+ ---
23
+
24
+ <a href={fullUrl} target="_blank" rel="noopener noreferrer"><slot /></a>
@@ -0,0 +1,18 @@
1
+ <div class="summary">
2
+ <slot />
3
+ </div>
4
+
5
+ <style>
6
+ .summary {
7
+ font-size: 1.15rem;
8
+ line-height: 1.6;
9
+ color: var(--sl-color-gray-2);
10
+ margin-bottom: 2rem;
11
+ }
12
+
13
+ .summary :global(p) {
14
+ font-size: inherit;
15
+ line-height: inherit;
16
+ color: inherit;
17
+ }
18
+ </style>
@@ -0,0 +1,23 @@
1
+ export { default as Api } from './Api.astro';
2
+ export { default as BeforeAfter } from './BeforeAfter.astro';
3
+ export { default as Details } from './Details.astro';
4
+ export { default as Enhancement } from './Enhancement.astro';
5
+ export { default as Feature } from './Feature.astro';
6
+ export { default as Figure } from './Figure.astro';
7
+ export { default as Fix } from './Fix.astro';
8
+ export { default as GallerySlider } from './GallerySlider.astro';
9
+ export { default as GamefaceDocsSidebar } from './GamefaceDocsSidebar.astro';
10
+ export { default as Highlight } from './Highlight.astro';
11
+ export { default as If } from './If.astro';
12
+ export { default as IfEnv } from './IfEnv.astro';
13
+ export { default as IfNot } from './IfNot.astro';
14
+ export { default as IncludeSnippets } from './IncludeSnippets.astro';
15
+ export { default as Internal } from './Internal.astro';
16
+ export { default as Link } from './Link.astro';
17
+ export { default as LinkCardGrid } from './LinkCardGrid.astro';
18
+ export { default as NativeLink } from './NativeLink.astro';
19
+ export { default as ProductName } from './ProductName.astro';
20
+ export { default as Release } from './Release.astro';
21
+ export { default as SideBarWithDropdown } from './SideBarWithDropdown.astro';
22
+ export { default as Summary } from './Summary.astro';
23
+ export { default as Typeref } from './Typeref.astro';
package/index.ts CHANGED
@@ -10,33 +10,71 @@ import { fileURLToPath } from 'url';
10
10
  import { directives } from './remark-directives';
11
11
  import { getSortedCoherentReleases } from './utils/coherentReleases';
12
12
  import { version } from './package.json';
13
+ import { remarkFixAbsoluteLinks } from './remark-directives/fixAbsoluteLinks';
13
14
 
14
15
  const __filename = fileURLToPath(import.meta.url);
15
16
  const __dirname = path.dirname(__filename);
16
17
  const defaultHeaderLinks = [
17
- { href: 'https://docs.coherent-labs.com/cpp-gameface', label: 'Gameface' },
18
- { href: 'https://docs.coherent-labs.com/cpp-prysm', label: 'Prysm' },
18
+ { href: 'https://docs.coherent-labs.com/cpp-gameface', label: 'Gameface', subDocumentations: ['Unreal', 'Unity'] },
19
+ { href: 'https://docs.coherent-labs.com/cpp-prysm', label: 'Prysm', subDocumentations: ['Unreal', 'Unity'] },
19
20
  { href: 'https://starter.coherent-labs.com/', label: 'UI Starter Guide' },
20
21
  { href: 'https://frontend-tools.coherent-labs.com', label: 'UI Tools' },
21
22
  { href: 'https://gameface-ui.coherent-labs.com', label: 'Gameface UI' },
22
23
  { href: 'https://coherent-labs.com/Documentation/ExporterLTS/', label: 'Adobe CC Tools' },
23
24
  ];
25
+
24
26
  const defaultMergeIndex = [
25
27
  {
26
28
  bundlePath: "https://gameface-ui.coherent-labs.com/pagefind",
27
29
  indexWeight: 0.5,
28
- mergeFilter: {
29
- resource: "Gameface UI"
30
- }
30
+ mergeFilter: { resource: "Gameface UI" }
31
31
  },
32
32
  {
33
33
  bundlePath: "https://frontend-tools.coherent-labs.com/pagefind",
34
34
  indexWeight: 0.5,
35
- mergeFilter: {
36
- resource: "UI Tools"
37
- }
35
+ mergeFilter: { resource: "UI Tools" }
36
+ },
37
+ {
38
+ bundlePath: "https://docs.coherent-labs.com/cpp-gameface/pagefind",
39
+ indexWeight: 0.5,
40
+ mergeFilter: { resource: "Gameface Custom Engine" }
41
+ },
42
+ {
43
+ bundlePath: "https://docs.coherent-labs.com/cpp-prysm/pagefind",
44
+ indexWeight: 0.5,
45
+ mergeFilter: { resource: "Prysm Custom Engine" }
46
+ },
47
+ {
48
+ bundlePath: "https://docs.coherent-labs.com/unreal-gameface/pagefind",
49
+ indexWeight: 0.5,
50
+ mergeFilter: { resource: "Gameface Unreal" }
51
+ },
52
+ {
53
+ bundlePath: "https://docs.coherent-labs.com/unreal-prysm/pagefind",
54
+ indexWeight: 0.5,
55
+ mergeFilter: { resource: "Prysm Unreal" }
56
+ },
57
+ {
58
+ bundlePath: "https://docs.coherent-labs.com/unity-gameface/pagefind",
59
+ indexWeight: 0.5,
60
+ mergeFilter: { resource: "Gameface Unity" }
61
+ },
62
+ {
63
+ bundlePath: "https://docs.coherent-labs.com/unity-prysm/pagefind",
64
+ indexWeight: 0.5,
65
+ mergeFilter: { resource: "Prysm Unity" }
38
66
  }
39
- ]
67
+ ];
68
+
69
+ async function isPagefindIndexAvailable(bundlePath: string): Promise<boolean> {
70
+ try {
71
+ const metaUrl = `${bundlePath.replace(/\/$/, '')}/pagefind-entry.json`;
72
+ const response = await fetch(metaUrl, { method: 'HEAD' });
73
+ return response.ok;
74
+ } catch (e) {
75
+ return false;
76
+ }
77
+ }
40
78
 
41
79
  export default function coherentThemePlugin(options: CoherentThemeOptions = { documentationSearchTag: '' }): StarlightPlugin[] {
42
80
  if (!options?.documentationSearchTag) {
@@ -56,7 +94,7 @@ export default function coherentThemePlugin(options: CoherentThemeOptions = { do
56
94
  const corePlugin: StarlightPlugin = {
57
95
  name: 'coherent-docs-theme',
58
96
  hooks: {
59
- 'config:setup'({ config, logger, updateConfig, addIntegration }) {
97
+ async 'config:setup'({ config, astroConfig, logger, updateConfig, addIntegration, command }) {
60
98
  logger.info(`Initializing Coherent Theme v${version}...`);
61
99
 
62
100
  addIntegration({
@@ -65,24 +103,34 @@ export default function coherentThemePlugin(options: CoherentThemeOptions = { do
65
103
  'astro:config:setup': ({ updateConfig }) => {
66
104
  updateConfig({
67
105
  markdown: {
68
- remarkPlugins: [...directives],
106
+ remarkPlugins: [...directives, [remarkFixAbsoluteLinks, { basePath: astroConfig.base }]],
69
107
  },
70
108
  });
71
109
  }
72
110
  }
73
111
  });
74
112
 
75
- process.env.COHERENT_THEME_CONFIG = JSON.stringify({ showPageProgress, navLinks, documentationSearchTag: options.documentationSearchTag });
113
+ process.env.COHERENT_THEME_CONFIG = JSON.stringify({
114
+ showPageProgress,
115
+ navLinks,
116
+ documentationSearchTag: options.documentationSearchTag,
117
+ tagManagerId: options.tagManagerId,
118
+ breadcrumbs: options.breadcrumbs,
119
+ topicsConfig: options.topicsConfig,
120
+ currentTopicId: options.currentTopicId,
121
+ version: options.version
122
+ });
76
123
 
77
124
  const configUpdates: any = {
78
125
  customCss: [...(config.customCss ?? []), 'coherent-docs-theme/styles'],
79
126
  components: overrideComponents(
80
127
  config,
81
- ['Header', 'ThemeSelect', 'Footer', 'Search'],
128
+ ['Header', 'ThemeSelect', 'Footer', 'Search', 'PageTitle', 'MarkdownContent'],
82
129
  logger,
83
130
  ),
84
131
  head: config.head || [],
85
132
  };
133
+
86
134
  if (!disableDefaultLogo && !config.logo) {
87
135
  configUpdates.logo = {
88
136
  dark: path.join(__dirname, 'assets/gameface-ui-header-dark.svg'),
@@ -97,15 +145,37 @@ export default function coherentThemePlugin(options: CoherentThemeOptions = { do
97
145
  'data-pagefind-filter': 'resource[content]',
98
146
  content: options.documentationSearchTag
99
147
  }
100
- })
101
-
102
- configUpdates.pagefind = {
103
- indexWeight: 2,
104
- mergeIndex: defaultMergeIndex.filter(merge => {
105
- const resource = merge.mergeFilter.resource;
106
- return resource !== options.documentationSearchTag;
107
- })
108
- };
148
+ });
149
+
150
+ if (command === 'build') {
151
+ const otherIndexes = defaultMergeIndex.filter(merge =>
152
+ merge.mergeFilter.resource !== options.documentationSearchTag
153
+ );
154
+
155
+ logger.info('Validating external Pagefind indexes for production build...');
156
+ const validMergeIndexes = [];
157
+
158
+ for (const mergeConfig of otherIndexes) {
159
+ const isAvailable = await isPagefindIndexAvailable(mergeConfig.bundlePath);
160
+ if (isAvailable) {
161
+ validMergeIndexes.push(mergeConfig);
162
+ logger.info(`✅ Found index: ${mergeConfig.mergeFilter.resource}`);
163
+ } else {
164
+ logger.warn(`⚠️ Skipping index (not found): ${mergeConfig.mergeFilter.resource} at ${mergeConfig.bundlePath}`);
165
+ }
166
+ }
167
+
168
+ configUpdates.pagefind = {
169
+ indexWeight: 2,
170
+ mergeIndex: validMergeIndexes
171
+ };
172
+ } else {
173
+ logger.info('Dev mode detected. Skipping external Pagefind index validation.');
174
+ configUpdates.pagefind = {
175
+ indexWeight: 2,
176
+ mergeIndex: []
177
+ };
178
+ }
109
179
 
110
180
  updateConfig(configUpdates);
111
181
  },
@@ -1,13 +1,43 @@
1
+ interface Topic {
2
+ label?: string
3
+ href?: string
4
+ icon?: string
5
+ }
6
+
1
7
  export interface CoherentThemeOptions {
2
- documentationSearchTag: string
8
+ documentationSearchTag:
9
+ 'Gameface Custom Engine' | 'Prysm Custom Engine' |
10
+ 'Gameface Unreal' | 'Prysm Unreal' |
11
+ 'Gameface Unity' | 'Prysm Unity' |
12
+ 'UI Tools' |
13
+ 'Gameface UI' | string
14
+ topicsConfig?: {
15
+ native?: Topic
16
+ unreal?: Topic
17
+ unity?: Topic
18
+ }
19
+ version?: string
20
+ currentTopicId?: string
3
21
  showPageProgress?: boolean;
4
- navLinks?: Array<{ label: string; href: string }>;
22
+ navLinks?: Array<{ label: string; href: string, subDocumentations?: string[] }>;
5
23
  disableDefaultLogo?: boolean;
6
24
  replacesTitle?: boolean
25
+ tagManagerId?: string
26
+ breadcrumbs?: boolean
7
27
  }
8
28
 
9
29
  export default function getThemeConfig(): CoherentThemeOptions {
10
- let themeConfig = { documentationSearchTag: '', showPageProgress: false, navLinks: [], disableDefaultLogo: false } as CoherentThemeOptions;
30
+ let themeConfig = {
31
+ documentationSearchTag: '',
32
+ showPageProgress: false,
33
+ navLinks: [],
34
+ disableDefaultLogo: false,
35
+ tagManagerId: '',
36
+ breadcrumbs: true,
37
+ topicsConfig: {},
38
+ currentTopicId: 'native',
39
+ version: '0.0.0.0'
40
+ } as CoherentThemeOptions;
11
41
 
12
42
  if (process.env.COHERENT_THEME_CONFIG) {
13
43
  try {
@@ -0,0 +1,12 @@
1
+ <svg
2
+ width="14"
3
+ height="14"
4
+ viewBox="0 0 24 24"
5
+ fill="none"
6
+ stroke="currentColor"
7
+ stroke-width="2"
8
+ stroke-linecap="round"
9
+ stroke-linejoin="round"
10
+ >
11
+ <polyline points="6 9 12 15 18 9"></polyline>
12
+ </svg>
@@ -9,8 +9,19 @@ interface Props {
9
9
  const { type = "header" } = Astro.props;
10
10
  const { navLinks, documentationSearchTag } = getThemeConfig();
11
11
 
12
- const isLinkActive = (href: string, label: string) => {
12
+ const isLinkActive = (href: string, label: string, subDocumentations?: string[]) => {
13
13
  try {
14
+ if (documentationSearchTag) {
15
+ if (documentationSearchTag === label) return true;
16
+
17
+ if (subDocumentations && subDocumentations.length > 0) {
18
+ const possibleTags = subDocumentations.map(sub => `${label} ${sub}`);
19
+ if (possibleTags.includes(documentationSearchTag)) {
20
+ return true;
21
+ }
22
+ }
23
+ }
24
+
14
25
  const linkUrl = new URL(href, Astro.url.origin);
15
26
  const currentUrl = Astro.url;
16
27
  const isLocal =
@@ -39,7 +50,7 @@ const navClasses =
39
50
  : "extra-links mobile sidebar-mode lg:hidden";
40
51
 
41
52
  const activeLinkObj = navLinks?.find((link) =>
42
- isLinkActive(link.href, link.label),
53
+ isLinkActive(link.href, link.label, link.subDocumentations),
43
54
  );
44
55
  const activeLabel = activeLinkObj ? activeLinkObj.label : "Documentations";
45
56
  ---
@@ -61,7 +72,7 @@ const activeLabel = activeLinkObj ? activeLinkObj.label : "Documentations";
61
72
  <li class="nav-item">
62
73
  <a
63
74
  href={link.href}
64
- class={`nav-link ${isLinkActive(link.href, link.label) ? "active" : ""}`}
75
+ class={`nav-link ${isLinkActive(link.href, link.label, link.subDocumentations) ? "active" : ""}`}
65
76
  >
66
77
  {link.label}
67
78
  </a>
@@ -80,7 +91,7 @@ const activeLabel = activeLinkObj ? activeLinkObj.label : "Documentations";
80
91
  <li>
81
92
  <a
82
93
  href={link.href}
83
- class={`dropdown-link ${isLinkActive(link.href, link.label) ? "active" : ""}`}
94
+ class={`dropdown-link ${isLinkActive(link.href, link.label, link.subDocumentations) ? "active" : ""}`}
84
95
  >
85
96
  {link.label}
86
97
  </a>