@astronox/core 0.5.3 → 0.5.5

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/README.md CHANGED
@@ -48,6 +48,19 @@ Use the remaining entry points for layouts, patterns, primitives, display
48
48
  contracts, configuration, theme, client behavior, data validation, and SEO.
49
49
  Deep imports into the physical `src` tree are not part of the public API.
50
50
 
51
+ `AnnotatedText` renders `DisplayAnnotatedText` spans with optional emphasis and
52
+ explanatory tooltips. `Tooltip` accepts either its existing string content or a
53
+ `DisplayTooltip` with a title and non-interactive text spans. `TextBlock`,
54
+ `InlineDefinition` and table descriptions also accept annotated text; table
55
+ tooltip bodies never create nested interactions. These contracts contain no
56
+ markup parser or product-specific vocabulary.
57
+
58
+ Annotated text inherits its tone and whitespace behavior from its context;
59
+ `data-tone` can override the tone at its public root. Empty or whitespace-only
60
+ segments never create tooltip buttons. `TextBlock preserveBreaks` applies the
61
+ same paragraph boundaries to plain and annotated text, even when a separator
62
+ crosses annotation boundaries, while retaining each segment's annotations.
63
+
51
64
  Locale catalogs, localized routing, preference persistence, localized metadata,
52
65
  and responsive media presentation are available through the `i18n`, `routing`,
53
66
  `seo`, and `display` entry points. Consuming sites provide their own locale
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astronox/core",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "Reusable Astro components, patterns, styles, and infrastructure for AstroNox sites.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -54,18 +54,17 @@ const adBlockingStorageKey = adBlockingRecovery?.storageKey ?? '';
54
54
  storageAvailable = false;
55
55
  }
56
56
 
57
- let cooldownActive =
57
+ const cooldownActive =
58
58
  storedTimestamp !== undefined &&
59
59
  now - storedTimestamp < adBlockingRedisplayIntervalMs;
60
+ let cooldownPersisted = false;
60
61
 
61
- const startCooldown = () => {
62
- if (cooldownActive) return;
63
-
64
- cooldownActive = true;
65
- if (!storageAvailable) return;
62
+ const persistCooldown = () => {
63
+ if (cooldownActive || cooldownPersisted || !storageAvailable) return;
66
64
 
67
65
  try {
68
66
  window.localStorage.setItem(adBlockingStorageKey, String(Date.now()));
67
+ cooldownPersisted = true;
69
68
  } catch {
70
69
  storageAvailable = false;
71
70
  }
@@ -89,7 +88,7 @@ const adBlockingStorageKey = adBlockingRecovery?.storageKey ?? '';
89
88
  return;
90
89
  }
91
90
 
92
- startCooldown();
91
+ persistCooldown();
93
92
  message.proceed(true);
94
93
  };
95
94
  }
@@ -0,0 +1,38 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types';
3
+ import type { DisplayAnnotatedText } from '../../../display/annotatedText';
4
+ import type { Tone } from '../../../theme/contracts';
5
+ import Tooltip from '../../feedback/Tooltip/Tooltip.astro';
6
+ import MarkedText from './MarkedText.astro';
7
+
8
+ type Props = Omit<HTMLAttributes<'span'>, 'content'> & {
9
+ 'data-tone'?: Tone;
10
+ content: DisplayAnnotatedText;
11
+ interactive?: boolean;
12
+ };
13
+ const { content, interactive = true, class: className, ...attrs } = Astro.props;
14
+ ---
15
+
16
+ <span
17
+ {...attrs}
18
+ class:list={['annotated-text', className]}
19
+ data-widget="annotated-text"
20
+ >
21
+ {
22
+ content.map((span) =>
23
+ span.tooltip && interactive && span.text.trim().length > 0 ? (
24
+ <Tooltip content={span.tooltip}>
25
+ <button
26
+ type="button"
27
+ class="annotated-text__term"
28
+ data-tooltip-trigger
29
+ >
30
+ {span.text}
31
+ </button>
32
+ </Tooltip>
33
+ ) : (
34
+ <MarkedText spans={[span]} />
35
+ ),
36
+ )
37
+ }
38
+ </span>
@@ -0,0 +1,19 @@
1
+ .annotated-text {
2
+ white-space: inherit;
3
+ }
4
+
5
+ .annotated-text__term {
6
+ display: inline;
7
+ margin: 0;
8
+ padding: 0;
9
+ border: 0;
10
+ background: none;
11
+ color: var(--nox-tone-text);
12
+ font: inherit;
13
+ font-weight: var(--nox-font-weight-strong);
14
+ text-align: inherit;
15
+ white-space: inherit;
16
+ text-decoration: underline dotted;
17
+ text-underline-offset: 0.2em;
18
+ cursor: help;
19
+ }
@@ -0,0 +1,11 @@
1
+ ---
2
+ import type { DisplayTextSpan } from '../../../display/annotatedText';
3
+ type Props = { spans: readonly DisplayTextSpan[] };
4
+ const { spans } = Astro.props;
5
+ ---
6
+
7
+ {
8
+ spans.map((span) =>
9
+ span.emphasized ? <strong>{span.text}</strong> : span.text,
10
+ )
11
+ }
@@ -1,11 +1,13 @@
1
1
  ---
2
2
  import type { HTMLAttributes } from 'astro/types';
3
3
  import type { Tone } from '../../../theme/contracts';
4
+ import type { DisplayAnnotatedText } from '../../../display/annotatedText';
5
+ import AnnotatedText from '../AnnotatedText/AnnotatedText.astro';
4
6
 
5
7
  type Props = HTMLAttributes<'p'> & {
6
8
  'data-tone'?: Tone;
7
9
  term: string;
8
- text?: string;
10
+ text?: string | DisplayAnnotatedText;
9
11
  };
10
12
 
11
13
  const {
@@ -26,6 +28,13 @@ const hasText = text !== undefined && text.length > 0;
26
28
  >
27
29
  <strong class="inline-definition__term">{term}</strong>
28
30
  {hasText ? <span class="inline-definition__separator">:</span> : null}
29
- {hasText ? <span class="inline-definition__text"> {text}</span> : null}
31
+ {
32
+ hasText && text !== undefined ? (
33
+ <span class="inline-definition__text">
34
+ {' '}
35
+ {typeof text === 'string' ? text : <AnnotatedText content={text} />}
36
+ </span>
37
+ ) : null
38
+ }
30
39
  {!hasText ? <slot /> : null}
31
40
  </p>
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  import type { HTMLAttributes } from 'astro/types';
3
+ import type { DisplayAnnotatedText } from '../../../display/annotatedText';
4
+ import AnnotatedText from '../../content/AnnotatedText/AnnotatedText.astro';
3
5
  import type {
4
6
  DisplayTextValue,
5
7
  TextColor,
@@ -13,7 +15,8 @@ type Props = HTMLAttributes<'span'> & {
13
15
  primaryColor?: TextColor;
14
16
  primarySize?: TextSize;
15
17
  primaryWeight?: TextWeight;
16
- secondary?: DisplayTextValue;
18
+ secondary?: DisplayTextValue | DisplayAnnotatedText;
19
+ interactiveSecondary?: boolean;
17
20
  secondaryWeight?: TextWeight;
18
21
  layout?: TextPairLayout;
19
22
  truncate?: boolean;
@@ -27,6 +30,7 @@ const {
27
30
  primarySize = 'default',
28
31
  primaryWeight = 'strong',
29
32
  secondary,
33
+ interactiveSecondary = true,
30
34
  secondaryWeight = 'medium',
31
35
  truncate = false,
32
36
  ...attrs
@@ -56,7 +60,14 @@ const {
56
60
  data-part="text-pair-secondary"
57
61
  data-text-weight={secondaryWeight}
58
62
  >
59
- {secondary}
63
+ {typeof secondary === 'object' ? (
64
+ <AnnotatedText
65
+ content={secondary}
66
+ interactive={interactiveSecondary}
67
+ />
68
+ ) : (
69
+ secondary
70
+ )}
60
71
  </span>
61
72
  ) : null
62
73
  }
@@ -1,11 +1,13 @@
1
1
  ---
2
2
  import type { HTMLAttributes } from 'astro/types';
3
3
  import type { Tone } from '../../../theme/contracts';
4
+ import type { DisplayTooltip } from '../../../display/annotatedText';
5
+ import MarkedText from '../../content/AnnotatedText/MarkedText.astro';
4
6
 
5
- type Props = HTMLAttributes<'span'> & {
7
+ type Props = Omit<HTMLAttributes<'span'>, 'content'> & {
6
8
  activation?: 'interactive' | 'hover';
7
9
  alignTo?: 'boundary' | 'trigger';
8
- content: string;
10
+ content: string | DisplayTooltip;
9
11
  tone?: Tone;
10
12
  triggerScope?: 'ancestor' | 'content';
11
13
  };
@@ -40,7 +42,20 @@ const resolvedTone = tone ?? dataTone;
40
42
  popover="manual"
41
43
  role="tooltip"
42
44
  >
43
- {content}
45
+ {
46
+ typeof content === 'string' ? (
47
+ content
48
+ ) : (
49
+ <>
50
+ {content.title ? (
51
+ <strong class="tooltip__title">{content.title}</strong>
52
+ ) : null}
53
+ <span class="tooltip__text">
54
+ <MarkedText spans={content.content} />
55
+ </span>
56
+ </>
57
+ )
58
+ }
44
59
  </span>
45
60
  </nox-tooltip>
46
61
 
@@ -50,6 +50,15 @@
50
50
  display: none;
51
51
  }
52
52
 
53
+ .tooltip__title {
54
+ display: block;
55
+ margin-block-end: var(--nox-space-2);
56
+ }
57
+
58
+ .tooltip__text {
59
+ white-space: pre-line;
60
+ }
61
+
53
62
  @media (width < 30rem) {
54
63
  .tooltip__panel {
55
64
  --tooltip-viewport-gap: var(--nox-space-3);
@@ -11,6 +11,7 @@ export { default as CardHeader } from './content/CardHeader/CardHeader.astro';
11
11
  export { default as CardLink } from './content/CardLink/CardLink.astro';
12
12
  export { default as CodeBlock } from './content/CodeBlock/CodeBlock.astro';
13
13
  export { default as InlineDefinition } from './content/InlineDefinition/InlineDefinition.astro';
14
+ export { default as AnnotatedText } from './content/AnnotatedText/AnnotatedText.astro';
14
15
  export { default as MarkdownContent } from './content/MarkdownContent/MarkdownContent.astro';
15
16
  export { default as BadgeList } from './data-display/BadgeList/BadgeList.astro';
16
17
  export { default as IconList } from './data-display/IconList/IconList.astro';
@@ -0,0 +1,14 @@
1
+ /** Content-only spans; tooltip bodies deliberately cannot contain interactions. */
2
+ export type DisplayTextSpan = {
3
+ text: string;
4
+ emphasized?: boolean;
5
+ };
6
+
7
+ export type DisplayTooltip = {
8
+ title?: string;
9
+ content: readonly DisplayTextSpan[];
10
+ };
11
+
12
+ export type DisplayAnnotatedText = readonly (DisplayTextSpan & {
13
+ tooltip?: DisplayTooltip;
14
+ })[];
@@ -1,5 +1,6 @@
1
1
  import type { AstroComponent } from '../components/widgetTypes';
2
2
  import type { Tone } from '../theme/contracts';
3
+ import type { DisplayAnnotatedText, DisplayTooltip } from './annotatedText';
3
4
 
4
5
  export type DisplayTextValue = string | number;
5
6
 
@@ -217,12 +218,12 @@ export type TableAction = TableActionBase &
217
218
  );
218
219
 
219
220
  export type TableCell = {
220
- tooltip?: string;
221
+ tooltip?: string | DisplayTooltip;
221
222
  } & (
222
223
  | {
223
224
  kind: 'text';
224
225
  value: DisplayTextValue;
225
- description?: string;
226
+ description?: string | DisplayAnnotatedText;
226
227
  layout?: TextPairLayout;
227
228
  primaryColor?: TextColor;
228
229
  primarySize?: TextSize;
@@ -233,7 +234,7 @@ export type TableCell = {
233
234
  kind: 'link';
234
235
  label: string;
235
236
  href: string;
236
- description?: string;
237
+ description?: string | DisplayAnnotatedText;
237
238
  layout?: TextPairLayout;
238
239
  primaryColor?: TextColor;
239
240
  primarySize?: TextSize;
@@ -1,3 +1,4 @@
1
1
  export * from './contracts';
2
+ export * from './annotatedText';
2
3
  export * from './responsiveMedia';
3
4
  export * from './tableProgressive';
@@ -142,7 +142,13 @@ const stateMessage =
142
142
  data-table-column={column.id}
143
143
  data-table-column-role={column.role}
144
144
  data-tooltip-trigger={
145
- cell?.tooltip?.trim() ? true : undefined
145
+ (
146
+ typeof cell?.tooltip === 'string'
147
+ ? cell.tooltip.trim()
148
+ : cell?.tooltip
149
+ )
150
+ ? true
151
+ : undefined
146
152
  }
147
153
  >
148
154
  <TableCellContent
@@ -11,7 +11,8 @@ type Props = {
11
11
  };
12
12
 
13
13
  const { cell, rowHref, truncate = false, wrap = false }: Props = Astro.props;
14
- const tooltip = cell?.tooltip?.trim();
14
+ const tooltip =
15
+ typeof cell?.tooltip === 'string' ? cell.tooltip.trim() : cell?.tooltip;
15
16
  const layout =
16
17
  cell?.kind === 'text' || cell?.kind === 'link' ? cell.layout : undefined;
17
18
 
@@ -37,6 +38,7 @@ const rootAttributes = cell
37
38
  <div {...rootAttributes}>
38
39
  <TableCellValue
39
40
  cell={cell}
41
+ interactive={false}
40
42
  rowHref={rowHref}
41
43
  truncate={truncate}
42
44
  wrap={wrap}
@@ -10,12 +10,19 @@ import Button from '../../components/actions/Button/Button.astro';
10
10
 
11
11
  type Props = {
12
12
  cell: TableCell;
13
+ interactive?: boolean;
13
14
  rowHref?: string;
14
15
  truncate?: boolean;
15
16
  wrap: boolean;
16
17
  };
17
18
 
18
- const { cell, rowHref, truncate = false, wrap }: Props = Astro.props;
19
+ const {
20
+ cell,
21
+ interactive = true,
22
+ rowHref,
23
+ truncate = false,
24
+ wrap,
25
+ }: Props = Astro.props;
19
26
 
20
27
  const formatValue = (value: DisplayTextValue): string => String(value);
21
28
  ---
@@ -28,6 +35,7 @@ const formatValue = (value: DisplayTextValue): string => String(value);
28
35
  primarySize={cell.primarySize}
29
36
  primaryWeight={cell.primaryWeight ?? 'regular'}
30
37
  secondary={cell.description}
38
+ interactiveSecondary={interactive}
31
39
  secondaryWeight={cell.secondaryWeight}
32
40
  layout={cell.layout}
33
41
  truncate={truncate}
@@ -48,6 +56,7 @@ const formatValue = (value: DisplayTextValue): string => String(value);
48
56
  primarySize={cell.primarySize}
49
57
  primaryWeight={cell.primaryWeight}
50
58
  secondary={cell.description}
59
+ interactiveSecondary={false}
51
60
  secondaryWeight={cell.secondaryWeight}
52
61
  layout={cell.layout}
53
62
  truncate={truncate}
@@ -1,11 +1,13 @@
1
1
  ---
2
2
  import type { HTMLAttributes } from 'astro/types';
3
3
  import type { Tone } from '../../../theme/contracts';
4
+ import type { DisplayAnnotatedText } from '../../../display/annotatedText';
5
+ import AnnotatedText from '../../../components/content/AnnotatedText/AnnotatedText.astro';
4
6
 
5
7
  type Props = HTMLAttributes<'p'> & {
6
8
  'data-tone'?: Tone;
7
9
  preserveBreaks?: boolean;
8
- text: string;
10
+ text: string | DisplayAnnotatedText;
9
11
  };
10
12
 
11
13
  const {
@@ -15,7 +17,49 @@ const {
15
17
  text,
16
18
  ...attrs
17
19
  }: Props = Astro.props;
18
- const paragraphs = preserveBreaks ? text.split(/\r?\n(?:[ \t]*\r?\n)+/) : [];
20
+ function splitParagraphs(
21
+ content: DisplayAnnotatedText,
22
+ ): DisplayAnnotatedText[] {
23
+ const text = content.map((span) => span.text).join('');
24
+ const result: DisplayAnnotatedText[] = [];
25
+ let spanIndex = 0;
26
+ let spanStart = 0;
27
+
28
+ // Slice the original annotations using offsets in the complete text. Paragraph
29
+ // separators may cross span boundaries, including between CR and LF.
30
+ const slice = (start: number, end: number): DisplayAnnotatedText => {
31
+ const spans: Array<DisplayAnnotatedText[number]> = [];
32
+ while (spanIndex < content.length && spanStart < end) {
33
+ const span = content[spanIndex]!;
34
+ const spanEnd = spanStart + span.text.length;
35
+ const from = Math.max(start, spanStart);
36
+ const to = Math.min(end, spanEnd);
37
+ if (from < to) {
38
+ spans.push({
39
+ ...span,
40
+ text: span.text.slice(from - spanStart, to - spanStart),
41
+ });
42
+ }
43
+ if (spanEnd > end) break;
44
+ spanStart = spanEnd;
45
+ spanIndex += 1;
46
+ }
47
+ return spans;
48
+ };
49
+
50
+ let start = 0;
51
+ for (const separator of text.matchAll(/\r?\n(?:[ \t]*\r?\n)+/g)) {
52
+ result.push(slice(start, separator.index));
53
+ start = separator.index + separator[0].length;
54
+ }
55
+ result.push(slice(start, text.length));
56
+ return result;
57
+ }
58
+ const paragraphs = !preserveBreaks
59
+ ? []
60
+ : typeof text === 'string'
61
+ ? text.split(/\r?\n(?:[ \t]*\r?\n)+/)
62
+ : splitParagraphs(text);
19
63
  ---
20
64
 
21
65
  {
@@ -28,7 +72,13 @@ const paragraphs = preserveBreaks ? text.split(/\r?\n(?:[ \t]*\r?\n)+/) : [];
28
72
  data-tone={dataTone}
29
73
  >
30
74
  {paragraphs.map((paragraph) => (
31
- <p class="text-block__paragraph">{paragraph}</p>
75
+ <p class="text-block__paragraph">
76
+ {typeof paragraph === 'string' ? (
77
+ paragraph
78
+ ) : (
79
+ <AnnotatedText content={paragraph} />
80
+ )}
81
+ </p>
32
82
  ))}
33
83
  </div>
34
84
  ) : (
@@ -38,7 +88,7 @@ const paragraphs = preserveBreaks ? text.split(/\r?\n(?:[ \t]*\r?\n)+/) : [];
38
88
  data-widget="text-block"
39
89
  data-tone={dataTone}
40
90
  >
41
- {text}
91
+ {typeof text === 'string' ? text : <AnnotatedText content={text} />}
42
92
  </p>
43
93
  )
44
94
  }
@@ -28,6 +28,8 @@
28
28
  @import '../components/content/CardLink/CardLink.css' layer(components);
29
29
  @import '../components/content/InlineDefinition/InlineDefinition.css'
30
30
  layer(components);
31
+ @import '../components/content/AnnotatedText/AnnotatedText.css'
32
+ layer(components);
31
33
  @import '../components/content/MarkdownContent/MarkdownContent.css'
32
34
  layer(components);
33
35
  @import '../components/data-display/BadgeList/BadgeList.css' layer(components);