@uniweb/kit 0.10.7 → 0.10.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/kit",
3
- "version": "0.10.7",
3
+ "version": "0.10.8",
4
4
  "description": "Standard component library for Uniweb foundations",
5
5
  "type": "module",
6
6
  "exports": {
@@ -97,8 +97,15 @@
97
97
  }
98
98
 
99
99
  /* Anchors land clear of a fixed site header when something scrolls to them —
100
- a contents rail, or a link with a #fragment. */
101
- .prose :is(h1, h2, h3, h4) {
100
+ a contents rail, or a link with a #fragment.
101
+
102
+ Figures, tables and display math are on this list because they became anchor
103
+ targets: an authored `{#fig-cells}` is emitted onto the element, and a
104
+ cross-reference links to it. Without them a `[#eq-binomial]` scrolls its
105
+ equation flush to the viewport top and the sticky header covers it — which is
106
+ exactly what happened the first time the feature was clicked. Headings had
107
+ the rule and nothing else did, because until then nothing else was a target. */
108
+ .prose :is(h1, h2, h3, h4, figure, table, .math-display) {
102
109
  scroll-margin-top: calc(var(--header-height, 4rem) + 1rem);
103
110
  }
104
111
 
@@ -129,3 +136,15 @@
129
136
  .tml-sml-pad { padding-left: 0.05em; }
130
137
  math mtd { padding-top: 0.5ex; padding-bottom: 0.5ex; }
131
138
  math mtable.tml-jot mtd { padding-top: 0.7ex; padding-bottom: 0.7ex; }
139
+
140
+ /* AMS auto-numbering. Which equations number is the AUTHOR's choice, made in
141
+ LaTeX: `align` and `equation` number, `aligned` and the starred forms do not.
142
+ Without these two rules that choice was discarded -- `align` and `align*`
143
+ rendered identically, so an author who asked for numbers silently got none. */
144
+ .tml-eqn::before {
145
+ counter-increment: tmlEqnNo;
146
+ content: "(" counter(tmlEqnNo) ")";
147
+ }
148
+ body {
149
+ counter-reset: tmlEqnNo;
150
+ }
package/src/xref/Ref.jsx CHANGED
@@ -66,20 +66,39 @@ function renderEntry(entry, kindMeta) {
66
66
  return label ? `${label}${sep}${counter}` : counter
67
67
  }
68
68
 
69
+ /** A counter as a link to the element it numbers. */
70
+ function counterLink(entry) {
71
+ return (
72
+ <a key={entry.id} href={`#${entry.id}`}>
73
+ {entry.counterText}
74
+ </a>
75
+ )
76
+ }
77
+
78
+ /**
79
+ * Prose list punctuation: "1", "1 and 2", "1, 2, and 3".
80
+ *
81
+ * Takes and returns nodes rather than strings so each counter can be its own
82
+ * link. A cluster's LABEL is deliberately left outside them — "Figures" covers
83
+ * the whole group, and there is no single target it could point at.
84
+ */
85
+ function joinProse(nodes) {
86
+ if (nodes.length <= 1) return nodes
87
+ if (nodes.length === 2) return [nodes[0], ' and ', nodes[1]]
88
+ const out = []
89
+ nodes.forEach((node, i) => {
90
+ if (i > 0) out.push(i === nodes.length - 1 ? ', and ' : ', ')
91
+ out.push(node)
92
+ })
93
+ return out
94
+ }
95
+
69
96
  function renderGroupSameKind(entries, kindMeta) {
70
- if (entries.length === 1) {
71
- return renderEntry(entries[0], kindMeta)
72
- }
73
- const label = kindMeta?.labelPlural || kindMeta?.label || ''
97
+ const plural = entries.length > 1
98
+ const label = (plural ? kindMeta?.labelPlural || kindMeta?.label : kindMeta?.label) || ''
74
99
  const sep = kindMeta?.sep ?? ' '
75
- const counters = entries.map((e) => e.counterText)
76
- let body
77
- if (counters.length === 2) {
78
- body = counters.join(' and ')
79
- } else {
80
- body = counters.slice(0, -1).join(', ') + ', and ' + counters[counters.length - 1]
81
- }
82
- return label ? `${label}${sep}${body}` : body
100
+ const body = joinProse(entries.map(counterLink))
101
+ return label ? [label, sep, ...body] : body
83
102
  }
84
103
 
85
104
  export function Ref({ params, block }) {
@@ -119,41 +138,49 @@ export function Ref({ params, block }) {
119
138
  `[xref] mixed-kind cluster (${[...new Set(allKinds)].join(', ')}) — falling back to comma-separated rendering`,
120
139
  )
121
140
  }
122
- const parts = resolved.map((r) =>
123
- r.missing ? `[?${r.id}]` : renderEntry(r.entry, r.kindMeta),
124
- )
125
- return <span className="xref">{parts.join(', ')}{locator}</span>
141
+ // Each kind keeps its own singular label, and each resolved part links
142
+ // to its own target. A missing one stays visible text.
143
+ const parts = []
144
+ resolved.forEach((r, i) => {
145
+ if (i > 0) parts.push(', ')
146
+ if (r.missing) {
147
+ parts.push(`[?${r.id}]`)
148
+ return
149
+ }
150
+ const label = r.kindMeta?.label || ''
151
+ const sep = r.kindMeta?.sep ?? ' '
152
+ if (label) parts.push(label, sep)
153
+ parts.push(counterLink(r.entry))
154
+ })
155
+ return <span className="xref">{parts}{locator}</span>
126
156
  }
127
157
 
128
158
  const onlyResolved = resolved.filter((r) => !r.missing)
129
- const text = onlyResolved.length > 0
130
- ? renderGroupSameKind(
131
- onlyResolved.map((r) => r.entry),
132
- onlyResolved[0].kindMeta,
133
- )
134
- : ''
135
-
136
- const missingTail = resolved.filter((r) => r.missing).map((r) => `[?${r.id}]`).join(', ')
137
- const body = [text, missingTail].filter(Boolean).join(', ')
138
-
139
- // A reference to exactly one target links to it. The renderer emits the
140
- // author's `{#id}` onto the labelled element, so `#fig-cells` is a real
141
- // anchor in the page, in an EPUB, and in a Paged.js document.
142
- //
143
- // Only the single case. A cluster reads "Figures 1 and 2" — one href
144
- // could only point at one of them, and linking the whole phrase to the
145
- // first target is a worse answer than linking none of it. Making each
146
- // number its own link means `renderGroupSameKind` returning nodes rather
147
- // than a string; worth doing when someone needs it, not before.
159
+
160
+ // The single case links the WHOLE reference — "Equation 1", label and all,
161
+ // is one phrase naming one thing, so the label belongs inside the link.
162
+ // A cluster cannot do that: "Figures 1 and 2" has one label over two
163
+ // targets, so there each counter is its own link and the label is plain.
148
164
  if (onlyResolved.length === 1 && resolved.length === 1) {
165
+ const { entry, kindMeta } = onlyResolved[0]
149
166
  return (
150
- <a className="xref" href={`#${onlyResolved[0].id}`}>
151
- {body}{locator}
167
+ <a className="xref" href={`#${entry.id}`}>
168
+ {renderEntry(entry, kindMeta)}{locator}
152
169
  </a>
153
170
  )
154
171
  }
155
172
 
156
- return <span className="xref">{body}{locator}</span>
173
+ const body = onlyResolved.length > 0
174
+ ? renderGroupSameKind(
175
+ onlyResolved.map((r) => r.entry),
176
+ onlyResolved[0].kindMeta,
177
+ )
178
+ : []
179
+
180
+ const missing = resolved.filter((r) => r.missing).map((r) => `[?${r.id}]`)
181
+ const parts = missing.length > 0 && body.length > 0 ? [...body, ', ', ...missing] : [...body, ...missing]
182
+
183
+ return <span className="xref">{parts}{locator}</span>
157
184
  }
158
185
 
159
186
  export default Ref