@airalogy/aimd-renderer 2.5.0 → 2.6.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.
@@ -13,6 +13,13 @@ import type {
13
13
  } from "@airalogy/aimd-core/types"
14
14
  import type { AimdNode, QuizPreviewOptions, RenderContext } from "@airalogy/aimd-core/types"
15
15
  import type { ExtractedAimdFields } from "@airalogy/aimd-core/types"
16
+ import {
17
+ formatAimdExampleValue,
18
+ getAimdFieldDescription,
19
+ getAimdFieldDisplayLabel,
20
+ getAimdFieldExamples,
21
+ getAimdFieldTitle,
22
+ } from "@airalogy/aimd-core/utils"
16
23
  import type { AimdRendererI18nOptions, AimdRendererMessages } from "../locales"
17
24
  import type { AimdComponentRenderer, ElementRenderer, ShikiHighlighter, VueRendererOptions } from "../vue/vue-renderer"
18
25
  import type { AimdRendererOptions, RenderResult } from "./processor"
@@ -226,6 +233,74 @@ function buildScaleBandChildren(quizNode: AimdQuizNode): VNode[] {
226
233
  ]
227
234
  }
228
235
 
236
+ interface FieldMetadataHelp {
237
+ tooltip: string
238
+ description?: string
239
+ examples: string[]
240
+ }
241
+
242
+ function getFieldHelpText(definition: { kwargs?: Record<string, unknown> } | undefined): FieldMetadataHelp {
243
+ const description = getAimdFieldDescription(definition)
244
+ const examples = getAimdFieldExamples(definition)
245
+ .map(formatAimdExampleValue)
246
+ .map(example => example.trim())
247
+ .filter(Boolean)
248
+ const exampleText = examples.length > 0 ? `e.g. ${examples.join(", ")}` : undefined
249
+ const tooltipLines = [description, exampleText].filter((value): value is string => Boolean(value))
250
+
251
+ return {
252
+ tooltip: tooltipLines.join("\n"),
253
+ description,
254
+ examples,
255
+ }
256
+ }
257
+
258
+ function renderFieldMetadataPopover(help: FieldMetadataHelp): VNode | null {
259
+ if (!help.description && help.examples.length === 0) {
260
+ return null
261
+ }
262
+ const children: VNode[] = []
263
+ if (help.description) {
264
+ children.push(h("span", {
265
+ class: "aimd-field__metadata-popover-line",
266
+ }, help.description))
267
+ }
268
+ if (help.examples.length > 0) {
269
+ children.push(h("span", { class: "aimd-field__metadata-examples" }, [
270
+ h("span", { class: "aimd-field__metadata-examples-label" }, "e.g."),
271
+ ...help.examples.map((example, index) => h("span", {
272
+ key: `${index}-${example}`,
273
+ class: "aimd-field__metadata-example",
274
+ }, example)),
275
+ ]))
276
+ }
277
+ return h("span", {
278
+ class: "aimd-field__metadata-popover",
279
+ role: "tooltip",
280
+ }, children)
281
+ }
282
+
283
+ function renderFieldName(id: string, definition: { kwargs?: Record<string, unknown> } | undefined): VNode {
284
+ const displayTitle = getAimdFieldDisplayLabel(id, definition)
285
+ const hasCustomTitle = getAimdFieldTitle(definition) !== undefined && displayTitle !== id
286
+ const help = getFieldHelpText(definition)
287
+ const hasHelp = Boolean(help.description) || help.examples.length > 0
288
+
289
+ return h("span", {
290
+ class: [
291
+ "aimd-field__name",
292
+ (hasCustomTitle || hasHelp) ? "aimd-field__name--with-metadata" : undefined,
293
+ hasHelp ? "aimd-field__metadata-host" : undefined,
294
+ ],
295
+ tabindex: hasHelp ? 0 : undefined,
296
+ "aria-label": help.tooltip || undefined,
297
+ }, [
298
+ h("span", { class: "aimd-field__title" }, displayTitle),
299
+ hasCustomTitle ? h("span", { class: "aimd-field__key" }, id) : null,
300
+ renderFieldMetadataPopover(help),
301
+ ])
302
+ }
303
+
229
304
  /**
230
305
  * Render preview tag for AIMD field
231
306
  */
@@ -234,6 +309,7 @@ function renderPreviewTag(
234
309
  id: string,
235
310
  messages: AimdRendererMessages,
236
311
  columns?: string[],
312
+ definition?: { kwargs?: Record<string, unknown>, subvars?: Record<string, { kwargs?: Record<string, unknown> }> },
237
313
  ): VNode {
238
314
  const scopeKey = getScopeKey(scope)
239
315
  const scopeLabel = getAimdRendererScopeLabel(scope, messages)
@@ -241,17 +317,19 @@ function renderPreviewTag(
241
317
  // var_table: render tag with table preview inside
242
318
  if (scope === "var_table") {
243
319
  const children: VNode[] = [
244
- h("div", { class: "aimd-field__header" }, [
245
- h("span", { class: "aimd-field__scope" }, messages.scope.table),
246
- h("span", { class: "aimd-field__name" }, id),
247
- ]),
320
+ h("div", { class: "aimd-field__header" }, [
321
+ h("span", { class: "aimd-field__scope" }, messages.scope.table),
322
+ renderFieldName(id, definition),
323
+ ]),
248
324
  ]
249
325
  // Add table preview inside the container
250
326
  if (columns && columns.length > 0) {
251
327
  children.push(
252
328
  h("table", { class: "aimd-field__table-preview" }, [
253
329
  h("thead", [
254
- h("tr", columns.map(col => h("th", col))),
330
+ h("tr", columns.map(col => h("th", {
331
+ "data-column-id": col,
332
+ }, [renderFieldName(col, definition?.subvars?.[col])]))),
255
333
  ]),
256
334
  h("tbody", [
257
335
  h("tr", columns.map(() => h("td", "..."))),
@@ -272,10 +350,10 @@ function renderPreviewTag(
272
350
  "class": `aimd-field aimd-field--${classSuffix}`,
273
351
  "data-aimd-type": scopeKey,
274
352
  "data-aimd-id": id,
275
- }, [
276
- h("span", { class: "aimd-field__scope" }, scopeLabel),
277
- h("span", { class: "aimd-field__name" }, id),
278
- ])
353
+ }, [
354
+ h("span", { class: "aimd-field__scope" }, scopeLabel),
355
+ renderFieldName(id, definition),
356
+ ])
279
357
  }
280
358
 
281
359
  /**
@@ -299,19 +377,20 @@ function createAimdRenderers(options: UnifiedTokenRendererOptions): Record<strin
299
377
  resolveQuizPreviewOptions(getMode(), options.quizPreview)
300
378
 
301
379
  return {
302
- var: async (node, ctx, children) => {
303
- const varNode = node as AimdVarNode
304
- const { id, scope } = varNode
380
+ var: async (node, ctx, children) => {
381
+ const varNode = node as AimdVarNode
382
+ const { id, scope } = varNode
383
+ const definition = varNode.definition
305
384
 
306
- if (isPreview()) {
385
+ if (isPreview()) {
307
386
  if (PreviewRenderer) {
308
387
  return h(PreviewRenderer, { type: "var" }, {
309
388
  default: () => children,
310
389
  name: () => id,
311
390
  })
312
391
  }
313
- return renderPreviewTag(scope, id, messages)
314
- }
392
+ return renderPreviewTag(scope, id, messages, undefined, definition)
393
+ }
315
394
 
316
395
  // Edit mode
317
396
  if (getTokenProps && AIMDItem) {
@@ -325,12 +404,13 @@ function createAimdRenderers(options: UnifiedTokenRendererOptions): Record<strin
325
404
  }
326
405
  }
327
406
 
328
- return renderPreviewTag(scope, id, messages)
329
- },
407
+ return renderPreviewTag(scope, id, messages, undefined, definition)
408
+ },
330
409
 
331
- var_table: async (node, ctx, children) => {
332
- const tableNode = node as AimdVarTableNode
333
- const { id, scope, columns } = tableNode
410
+ var_table: async (node, ctx, children) => {
411
+ const tableNode = node as AimdVarTableNode
412
+ const { id, scope, columns } = tableNode
413
+ const definition = tableNode.definition
334
414
 
335
415
  if (isPreview()) {
336
416
  if (PreviewRenderer) {
@@ -340,8 +420,8 @@ function createAimdRenderers(options: UnifiedTokenRendererOptions): Record<strin
340
420
  })
341
421
  }
342
422
  // Preview mode: render inline tag with columns info
343
- return renderPreviewTag(scope, id, messages, columns)
344
- }
423
+ return renderPreviewTag(scope, id, messages, columns, definition)
424
+ }
345
425
 
346
426
  // Edit mode
347
427
  if (getTokenProps && AIMDTag) {
@@ -349,8 +429,8 @@ function createAimdRenderers(options: UnifiedTokenRendererOptions): Record<strin
349
429
  return h(AIMDTag, { ...item, props: columns })
350
430
  }
351
431
 
352
- return renderPreviewTag(scope, id, messages, columns)
353
- },
432
+ return renderPreviewTag(scope, id, messages, columns, definition)
433
+ },
354
434
 
355
435
  quiz: async (node, ctx, children) => {
356
436
  const quizNode = node as AimdQuizNode
package/src/index.ts CHANGED
@@ -51,9 +51,12 @@ export {
51
51
  createEmbeddedRenderer,
52
52
  createMermaidRenderer,
53
53
  createStepCardRenderer,
54
+ loadShikiHighlighter,
55
+ type CodeBlockRendererOptions,
54
56
  type ElementRenderer,
55
57
  hastToVue,
56
58
  renderToVNodes,
59
+ type LoadShikiHighlighterOptions,
57
60
  type AimdStepCardRendererOptions,
58
61
  type ShikiHighlighter,
59
62
  type VueRendererOptions,
@@ -1,2 +1,111 @@
1
1
  /* Re-export KaTeX base styles for AIMD renderer consumers. */
2
2
  @import "katex/dist/katex.min.css";
3
+
4
+ .aimd-field__metadata-host {
5
+ position: relative;
6
+ cursor: help;
7
+ outline: none;
8
+ }
9
+
10
+ .aimd-field__metadata-host:hover,
11
+ .aimd-field__metadata-host:focus,
12
+ .aimd-field__metadata-host:focus-within {
13
+ z-index: 90;
14
+ }
15
+
16
+ .aimd-field__metadata-host:focus-visible {
17
+ border-radius: 4px;
18
+ box-shadow: 0 0 0 2px rgba(65, 129, 253, 0.24);
19
+ }
20
+
21
+ .aimd-field__metadata-host .aimd-field__title {
22
+ text-decoration-line: underline;
23
+ text-decoration-style: dotted;
24
+ text-decoration-color: rgba(15, 23, 42, 0.36);
25
+ text-underline-offset: 3px;
26
+ }
27
+
28
+ .aimd-field__metadata-popover {
29
+ position: absolute;
30
+ z-index: 80;
31
+ inset-inline-start: 0;
32
+ top: calc(100% + 7px);
33
+ display: inline-flex;
34
+ flex-direction: column;
35
+ gap: 4px;
36
+ width: max-content;
37
+ min-width: 220px;
38
+ max-width: min(360px, 82vw);
39
+ padding: 8px 10px;
40
+ border-radius: 8px;
41
+ background: rgba(15, 23, 42, 0.96);
42
+ color: #f8fafc;
43
+ box-shadow: 0 12px 30px rgba(15, 23, 42, 0.28);
44
+ font-size: 12px;
45
+ font-weight: 500;
46
+ line-height: 1.45;
47
+ letter-spacing: 0;
48
+ text-align: left;
49
+ text-transform: none;
50
+ white-space: normal;
51
+ pointer-events: none;
52
+ opacity: 0;
53
+ visibility: hidden;
54
+ transform: translateY(-2px);
55
+ transition:
56
+ opacity 120ms ease,
57
+ transform 120ms ease,
58
+ visibility 120ms ease;
59
+ }
60
+
61
+ .aimd-field__metadata-popover::before {
62
+ content: "";
63
+ position: absolute;
64
+ inset-inline-start: 14px;
65
+ top: -5px;
66
+ width: 10px;
67
+ height: 10px;
68
+ background: rgba(15, 23, 42, 0.96);
69
+ transform: rotate(45deg);
70
+ }
71
+
72
+ .aimd-field__metadata-popover-line {
73
+ display: block;
74
+ color: inherit;
75
+ }
76
+
77
+ .aimd-field__metadata-examples {
78
+ display: flex;
79
+ flex-wrap: wrap;
80
+ align-items: center;
81
+ gap: 4px;
82
+ padding-top: 1px;
83
+ }
84
+
85
+ .aimd-field__metadata-examples-label {
86
+ color: rgba(226, 232, 240, 0.78);
87
+ }
88
+
89
+ .aimd-field__metadata-example {
90
+ display: inline-flex;
91
+ align-items: center;
92
+ max-width: 100%;
93
+ padding: 1px 6px;
94
+ border: 1px solid rgba(248, 250, 252, 0.18);
95
+ border-radius: 5px;
96
+ background: rgba(248, 250, 252, 0.1);
97
+ color: #f8fafc;
98
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
99
+ font-size: 11px;
100
+ font-weight: 600;
101
+ line-height: 1.45;
102
+ overflow-wrap: anywhere;
103
+ }
104
+
105
+ .aimd-field__metadata-host:hover > .aimd-field__metadata-popover,
106
+ .aimd-field__metadata-host:focus > .aimd-field__metadata-popover,
107
+ .aimd-field__metadata-host:focus-within > .aimd-field__metadata-popover {
108
+ opacity: 1;
109
+ visibility: visible;
110
+ transform: translateY(0);
111
+ }
package/src/vue/index.ts CHANGED
@@ -12,9 +12,12 @@ export {
12
12
  createEmbeddedRenderer,
13
13
  createMermaidRenderer,
14
14
  createStepCardRenderer,
15
+ loadShikiHighlighter,
16
+ type CodeBlockRendererOptions,
15
17
  type ElementRenderer,
16
18
  hastToVue,
17
19
  renderToVNodes,
20
+ type LoadShikiHighlighterOptions,
18
21
  type AimdStepCardRendererOptions,
19
22
  type ShikiHighlighter,
20
23
  type VueRendererOptions,