@tekkare/romulus 0.1.86 → 0.1.88
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/dist/module.json
CHANGED
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
et `ph-file-csv` ne se confondent pas en noir et blanc. Elle vient des tokens
|
|
13
13
|
du theme et non d'un hexadecimal en dur, pour suivre le mode nuit.
|
|
14
14
|
|
|
15
|
+
Elle passe par la prop `color` de RmIcon et non par une classe : le style
|
|
16
|
+
scope de ce fichier n'atteint pas le `<svg>` que RmIcon monte derriere son
|
|
17
|
+
`<ClientOnly>`, et l'icone retombait sur la couleur du texte. Les classes
|
|
18
|
+
posees sur RmIcon ne servent donc qu'a la mise en place, jamais a la
|
|
19
|
+
couleur.
|
|
20
|
+
|
|
15
21
|
Le composant ne navigue pas et n'ouvre rien lui-meme : il emet `select`, la
|
|
16
22
|
page decide (un apercu dans un tiroir, un onglet, un telechargement).
|
|
17
23
|
-->
|
|
@@ -46,18 +52,18 @@ const emit = defineEmits<{ select: [] }>()
|
|
|
46
52
|
* neutre dit « document » sans se tromper, ce qu'une icone approchante ne
|
|
47
53
|
* ferait pas.
|
|
48
54
|
*/
|
|
49
|
-
const FORMATS: { match: string[]; icon: string;
|
|
50
|
-
{ match: ['pdf'], icon: 'FilePdf',
|
|
51
|
-
{ match: ['csv'], icon: 'FileCsv',
|
|
52
|
-
{ match: ['sheet', 'excel', 'xls'], icon: 'FileXls',
|
|
53
|
-
{ match: ['word', 'doc'], icon: 'FileDoc',
|
|
55
|
+
const FORMATS: { match: string[]; icon: string; color: string }[] = [
|
|
56
|
+
{ match: ['pdf'], icon: 'FilePdf', color: 'var(--color-danger)' },
|
|
57
|
+
{ match: ['csv'], icon: 'FileCsv', color: 'var(--color-info)' },
|
|
58
|
+
{ match: ['sheet', 'excel', 'xls'], icon: 'FileXls', color: 'var(--color-success)' },
|
|
59
|
+
{ match: ['word', 'doc'], icon: 'FileDoc', color: 'var(--color-primary)' },
|
|
54
60
|
]
|
|
55
61
|
|
|
56
62
|
const format = computed(() => {
|
|
57
63
|
const value = props.format.toLowerCase()
|
|
58
64
|
return (
|
|
59
65
|
FORMATS.find(entry => entry.match.some(word => value.includes(word)))
|
|
60
|
-
?? { icon: 'FileText',
|
|
66
|
+
?? { icon: 'FileText', color: 'var(--text-muted)' }
|
|
61
67
|
)
|
|
62
68
|
})
|
|
63
69
|
|
|
@@ -70,23 +76,17 @@ const ACTION_ICONS: Record<string, string> = {
|
|
|
70
76
|
|
|
71
77
|
<template>
|
|
72
78
|
<button type="button" class="rm-file-row" @click="emit('select')">
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
:name="format.icon"
|
|
77
|
-
:size="16"
|
|
78
|
-
/>
|
|
79
|
+
<span class="rm-file-row__icon">
|
|
80
|
+
<RmIcon :name="format.icon" :size="16" :color="format.color" />
|
|
81
|
+
</span>
|
|
79
82
|
<span class="rm-file-row__name">{{ name }}</span>
|
|
80
83
|
<span v-if="size" class="rm-file-row__size">{{ size }}</span>
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
:name="ACTION_ICONS[action]"
|
|
85
|
-
:size="14"
|
|
86
|
-
/>
|
|
84
|
+
<span v-if="action !== 'none'" class="rm-file-row__action">
|
|
85
|
+
<RmIcon :name="ACTION_ICONS[action]" :size="14" color="var(--text-muted)" />
|
|
86
|
+
</span>
|
|
87
87
|
</button>
|
|
88
88
|
</template>
|
|
89
89
|
|
|
90
90
|
<style scoped>
|
|
91
|
-
.rm-file-row{align-items:center;background:transparent;border:none;border-radius:var(--radius-md);color:var(--text-primary);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:var(--font-13);gap:8px;line-height:1.4;padding:8px;text-align:left;transition:background-color var(--duration-fast) linear;width:100%}.rm-file-row:hover{background:var(--bg-surface-2)}.rm-file-
|
|
91
|
+
.rm-file-row{align-items:center;background:transparent;border:none;border-radius:var(--radius-md);color:var(--text-primary);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:var(--font-13);gap:8px;line-height:1.4;padding:8px;text-align:left;transition:background-color var(--duration-fast) linear;width:100%}.rm-file-row:hover{background:var(--bg-surface-2)}.rm-file-row__action,.rm-file-row__icon{display:inline-flex;flex:none}.rm-file-row__name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-file-row__size{color:var(--text-muted);flex:none;font-size:var(--font-12)}
|
|
92
92
|
</style>
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmNotes - le panneau « Notes », la documentation d'un ecran en un seul endroit.
|
|
3
|
+
|
|
4
|
+
Une app metier accumulait un onglet par genre d'explication : Infos,
|
|
5
|
+
Metrics, Tips, Methodologie. Quatre tuiles au rail pour quatre textes courts,
|
|
6
|
+
et le lecteur devait deviner sous laquelle sa reponse se trouvait — la
|
|
7
|
+
definition d'un indicateur pouvait aussi bien tenir dans « Metrics » que dans
|
|
8
|
+
« Infos ». Le panneau les rassemble : une seule tuile, une liste de notes,
|
|
9
|
+
et le genre devient une categorie qu'on filtre au lieu d'un onglet qu'on
|
|
10
|
+
cherche.
|
|
11
|
+
|
|
12
|
+
Le composant ne connait pas l'ecran : l'appelant lui donne les notes deja
|
|
13
|
+
triees pour la page ouverte, deja traduites. La recherche et les pastilles
|
|
14
|
+
travaillent sur cette liste en memoire, sans aller-retour.
|
|
15
|
+
|
|
16
|
+
Le temps de lecture est calcule a partir du texte des notes, jamais pose a la
|
|
17
|
+
main : une note qu'on rallonge annoncerait sinon une duree fausse. La date,
|
|
18
|
+
elle, vient de l'appelant — c'est une date de revision editoriale, que rien
|
|
19
|
+
dans le contenu ne permet de deviner.
|
|
20
|
+
-->
|
|
21
|
+
<script setup lang="ts">
|
|
22
|
+
import { computed, ref } from 'vue'
|
|
23
|
+
|
|
24
|
+
export type RmNoteTone = 'primary' | 'info' | 'warning' | 'success' | 'neutral'
|
|
25
|
+
|
|
26
|
+
export type RmNoteBlock =
|
|
27
|
+
/** Paragraphe. `**gras**` est reconnu, rien d'autre. */
|
|
28
|
+
| { type: 'text', text: string }
|
|
29
|
+
/** Liste a puces. Chaque entree accepte le meme gras. */
|
|
30
|
+
| { type: 'list', items: string[] }
|
|
31
|
+
/** Liste de definitions : le terme en gras, sa definition a la suite. */
|
|
32
|
+
| { type: 'terms', items: { term: string, def?: string }[] }
|
|
33
|
+
/** Encadre : un avertissement, une limite, une precaution de lecture. */
|
|
34
|
+
| { type: 'callout', text: string, tone?: 'info' | 'warning', title?: string }
|
|
35
|
+
/** Exemple, rendu en fonte a chasse fixe sur fond teinte. */
|
|
36
|
+
| { type: 'example', text: string }
|
|
37
|
+
/** Renvoi vers un ecran de l'app. Le clic remonte en `navigate`. */
|
|
38
|
+
| { type: 'link', to: string, label: string }
|
|
39
|
+
|
|
40
|
+
export interface RmNoteSection {
|
|
41
|
+
/** Intitule de section, en capitales dans la note depliee. */
|
|
42
|
+
title?: string
|
|
43
|
+
blocks: RmNoteBlock[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface RmNote {
|
|
47
|
+
id: string
|
|
48
|
+
title: string
|
|
49
|
+
/** Accroche lue liste repliee : ce que la note repond, en une phrase. */
|
|
50
|
+
summary: string
|
|
51
|
+
/** Cle de categorie, a rapprocher de `categories`. */
|
|
52
|
+
category: string
|
|
53
|
+
/** Date ISO de derniere revision editoriale. */
|
|
54
|
+
updated?: string
|
|
55
|
+
/** Icone Phosphor. Par defaut celle de la categorie. */
|
|
56
|
+
icon?: string
|
|
57
|
+
/** Teinte de la pastille. Par defaut celle de la categorie. */
|
|
58
|
+
tone?: RmNoteTone
|
|
59
|
+
sections: RmNoteSection[]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface RmNoteCategory {
|
|
63
|
+
key: string
|
|
64
|
+
label: string
|
|
65
|
+
icon?: string
|
|
66
|
+
tone?: RmNoteTone
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const props = withDefaults(defineProps<{
|
|
70
|
+
/** Notes de l'ecran ouvert, deja traduites et ordonnees. */
|
|
71
|
+
notes: RmNote[]
|
|
72
|
+
/** Categories connues, dans l'ordre des pastilles. Celles sans note sont masquees. */
|
|
73
|
+
categories?: RmNoteCategory[]
|
|
74
|
+
title?: string
|
|
75
|
+
/** Phrase d'introduction : qui ecrit ces notes, et ce qu'elles couvrent. */
|
|
76
|
+
description?: string
|
|
77
|
+
/** Pastille d'auteur en haut a droite (« TEKKARE »). Vide : pas de pastille. */
|
|
78
|
+
badge?: string
|
|
79
|
+
lang?: 'fr' | 'en'
|
|
80
|
+
}>(), {
|
|
81
|
+
categories: () => [],
|
|
82
|
+
title: 'Notes',
|
|
83
|
+
description: undefined,
|
|
84
|
+
badge: undefined,
|
|
85
|
+
lang: 'fr',
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const emit = defineEmits<{ navigate: [to: string] }>()
|
|
89
|
+
|
|
90
|
+
const WORDS = {
|
|
91
|
+
fr: {
|
|
92
|
+
all: 'Toutes',
|
|
93
|
+
search: 'Rechercher dans les notes...',
|
|
94
|
+
updated: 'MAJ',
|
|
95
|
+
reading: 'min de lecture',
|
|
96
|
+
expand: 'Déplier la note',
|
|
97
|
+
collapse: 'Replier la note',
|
|
98
|
+
emptyTitle: 'Aucune note pour cet écran',
|
|
99
|
+
emptyText: "Les notes sont rattachées à la page affichée : changez d'écran pour en voir d'autres.",
|
|
100
|
+
noMatch: 'Aucune note ne correspond',
|
|
101
|
+
noMatchText: 'Essayez un autre terme, ou revenez à « Toutes ».',
|
|
102
|
+
},
|
|
103
|
+
en: {
|
|
104
|
+
all: 'All',
|
|
105
|
+
search: 'Search the notes...',
|
|
106
|
+
updated: 'Updated',
|
|
107
|
+
reading: 'min read',
|
|
108
|
+
expand: 'Expand note',
|
|
109
|
+
collapse: 'Collapse note',
|
|
110
|
+
emptyTitle: 'No note for this screen',
|
|
111
|
+
emptyText: 'Notes belong to the page on display: switch screens to see others.',
|
|
112
|
+
noMatch: 'No note matches',
|
|
113
|
+
noMatchText: 'Try another term, or go back to "All".',
|
|
114
|
+
},
|
|
115
|
+
} as const
|
|
116
|
+
|
|
117
|
+
const T = computed(() => WORDS[props.lang] ?? WORDS.fr)
|
|
118
|
+
|
|
119
|
+
const search = ref('')
|
|
120
|
+
const category = ref('all')
|
|
121
|
+
const open = ref<string[]>([])
|
|
122
|
+
|
|
123
|
+
function toggle(id: string) {
|
|
124
|
+
open.value = open.value.includes(id)
|
|
125
|
+
? open.value.filter(item => item !== id)
|
|
126
|
+
: [...open.value, id]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const descriptor = computed(() => {
|
|
130
|
+
const map = new Map<string, RmNoteCategory>()
|
|
131
|
+
for (const item of props.categories) map.set(item.key, item)
|
|
132
|
+
return map
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
/** Tout le texte d'une note, pour la recherche et le temps de lecture. */
|
|
136
|
+
function plainText(note: RmNote) {
|
|
137
|
+
const parts = [note.title, note.summary]
|
|
138
|
+
for (const section of note.sections) {
|
|
139
|
+
if (section.title) parts.push(section.title)
|
|
140
|
+
for (const block of section.blocks) {
|
|
141
|
+
if (block.type === 'text' || block.type === 'callout' || block.type === 'example') parts.push(block.text)
|
|
142
|
+
else if (block.type === 'list') parts.push(block.items.join(' '))
|
|
143
|
+
else if (block.type === 'terms') parts.push(block.items.map(item => `${item.term} ${item.def ?? ''}`).join(' '))
|
|
144
|
+
else if (block.type === 'link') parts.push(block.label)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return parts.join(' ')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 200 mots par minute, une minute au minimum : « 0 min » ne veut rien dire. */
|
|
151
|
+
function readingTime(note: RmNote) {
|
|
152
|
+
const words = plainText(note).trim().split(/\s+/).filter(Boolean).length
|
|
153
|
+
return Math.max(1, Math.round(words / 200))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const shown = computed(() => {
|
|
157
|
+
const needle = search.value.trim().toLowerCase()
|
|
158
|
+
return props.notes.filter((note) => {
|
|
159
|
+
if (category.value !== 'all' && note.category !== category.value) return false
|
|
160
|
+
if (!needle) return true
|
|
161
|
+
return plainText(note).toLowerCase().includes(needle)
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
/** Pastilles : « Toutes » puis les seules categories qui ont une note ici. */
|
|
166
|
+
const chips = computed(() => {
|
|
167
|
+
const counts = new Map<string, number>()
|
|
168
|
+
for (const note of props.notes) counts.set(note.category, (counts.get(note.category) ?? 0) + 1)
|
|
169
|
+
|
|
170
|
+
const ordered = props.categories.length
|
|
171
|
+
? props.categories.filter(item => counts.has(item.key))
|
|
172
|
+
: [...counts.keys()].map(key => ({ key, label: key } as RmNoteCategory))
|
|
173
|
+
|
|
174
|
+
return [
|
|
175
|
+
{ key: 'all', label: T.value.all, icon: undefined, count: props.notes.length },
|
|
176
|
+
...ordered.map(item => ({ key: item.key, label: item.label, icon: item.icon, count: counts.get(item.key) ?? 0 })),
|
|
177
|
+
]
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
function noteIcon(note: RmNote) {
|
|
181
|
+
return note.icon ?? descriptor.value.get(note.category)?.icon ?? 'Note'
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function noteTone(note: RmNote): RmNoteTone {
|
|
185
|
+
return note.tone ?? descriptor.value.get(note.category)?.tone ?? 'primary'
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function categoryLabel(note: RmNote) {
|
|
189
|
+
return descriptor.value.get(note.category)?.label ?? note.category
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function updatedLabel(note: RmNote) {
|
|
193
|
+
const time = note.updated ? new Date(note.updated).getTime() : Number.NaN
|
|
194
|
+
if (!Number.isFinite(time) || time <= 0) return ''
|
|
195
|
+
return new Date(time).toLocaleDateString(props.lang === 'fr' ? 'fr-FR' : 'en-GB', {
|
|
196
|
+
day: 'numeric',
|
|
197
|
+
month: 'long',
|
|
198
|
+
year: 'numeric',
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Le seul balisage reconnu dans une note est `**gras**`. Le texte est decoupe
|
|
204
|
+
* en morceaux rendus par `v-for` plutot que par `v-html` : les notes viennent
|
|
205
|
+
* du code de l'app aujourd'hui, d'un CMS demain, et un panneau qui interprete
|
|
206
|
+
* du HTML est un panneau qui l'executera le jour ou la source change.
|
|
207
|
+
*/
|
|
208
|
+
function inline(text: string) {
|
|
209
|
+
return text.split(/(\*\*[^*]+\*\*)/g)
|
|
210
|
+
.filter(Boolean)
|
|
211
|
+
.map((chunk, index) => ({
|
|
212
|
+
key: index,
|
|
213
|
+
bold: chunk.startsWith('**') && chunk.endsWith('**'),
|
|
214
|
+
text: chunk.startsWith('**') && chunk.endsWith('**') ? chunk.slice(2, -2) : chunk,
|
|
215
|
+
}))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function onLink(to: string) {
|
|
219
|
+
emit('navigate', to)
|
|
220
|
+
}
|
|
221
|
+
</script>
|
|
222
|
+
|
|
223
|
+
<template>
|
|
224
|
+
<div class="rm-notes">
|
|
225
|
+
<div class="rm-notes__head">
|
|
226
|
+
<h2 v-if="title" class="rm-notes__title">{{ title }}</h2>
|
|
227
|
+
<span v-if="badge" class="rm-notes__badge">
|
|
228
|
+
<RmIcon name="ShieldCheck" :size="14" weight="fill" />
|
|
229
|
+
{{ badge }}
|
|
230
|
+
</span>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<p v-if="description" class="rm-notes__desc">{{ description }}</p>
|
|
234
|
+
|
|
235
|
+
<label class="rm-notes__search">
|
|
236
|
+
<RmIcon name="MagnifyingGlass" :size="14" />
|
|
237
|
+
<input v-model="search" type="text" :placeholder="T.search">
|
|
238
|
+
</label>
|
|
239
|
+
|
|
240
|
+
<div class="rm-notes__chips" role="group">
|
|
241
|
+
<button
|
|
242
|
+
v-for="chip in chips"
|
|
243
|
+
:key="chip.key"
|
|
244
|
+
type="button"
|
|
245
|
+
class="rm-notes__chip"
|
|
246
|
+
:class="{ 'rm-notes__chip--on': category === chip.key }"
|
|
247
|
+
:aria-pressed="category === chip.key"
|
|
248
|
+
@click="category = chip.key"
|
|
249
|
+
>
|
|
250
|
+
<RmIcon v-if="chip.icon" :name="chip.icon" :size="13" />
|
|
251
|
+
{{ chip.label }}
|
|
252
|
+
<span v-if="chip.key === 'all'" class="rm-notes__chip-count">{{ chip.count }}</span>
|
|
253
|
+
</button>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<RmEmptyState
|
|
257
|
+
v-if="!notes.length"
|
|
258
|
+
framed
|
|
259
|
+
tone="primary"
|
|
260
|
+
icon="Notebook"
|
|
261
|
+
:title="T.emptyTitle"
|
|
262
|
+
:text="T.emptyText"
|
|
263
|
+
/>
|
|
264
|
+
|
|
265
|
+
<RmEmptyState
|
|
266
|
+
v-else-if="!shown.length"
|
|
267
|
+
framed
|
|
268
|
+
icon="MagnifyingGlass"
|
|
269
|
+
:title="T.noMatch"
|
|
270
|
+
:text="T.noMatchText"
|
|
271
|
+
/>
|
|
272
|
+
|
|
273
|
+
<div v-else class="rm-notes__list">
|
|
274
|
+
<article
|
|
275
|
+
v-for="note in shown"
|
|
276
|
+
:key="note.id"
|
|
277
|
+
class="rm-notes__note"
|
|
278
|
+
:class="{ 'rm-notes__note--open': open.includes(note.id) }"
|
|
279
|
+
>
|
|
280
|
+
<header class="rm-notes__note-head">
|
|
281
|
+
<button
|
|
282
|
+
type="button"
|
|
283
|
+
class="rm-notes__note-id"
|
|
284
|
+
:aria-expanded="open.includes(note.id)"
|
|
285
|
+
:aria-controls="`rm-note-${note.id}`"
|
|
286
|
+
@click="toggle(note.id)"
|
|
287
|
+
>
|
|
288
|
+
<span class="rm-notes__note-icon" :class="`rm-notes__note-icon--${noteTone(note)}`">
|
|
289
|
+
<RmIcon :name="noteIcon(note)" :size="18" weight="fill" />
|
|
290
|
+
</span>
|
|
291
|
+
<span class="rm-notes__note-text">
|
|
292
|
+
<span class="rm-notes__note-title">{{ note.title }}</span>
|
|
293
|
+
<span class="rm-notes__note-meta">
|
|
294
|
+
<span class="rm-notes__tag" :class="`rm-notes__tag--${noteTone(note)}`">
|
|
295
|
+
{{ categoryLabel(note) }}
|
|
296
|
+
</span>
|
|
297
|
+
<span v-if="updatedLabel(note)" class="rm-notes__cell">
|
|
298
|
+
<RmIcon name="Clock" :size="13" />
|
|
299
|
+
{{ T.updated }} {{ updatedLabel(note) }}
|
|
300
|
+
</span>
|
|
301
|
+
<span class="rm-notes__cell">
|
|
302
|
+
<RmIcon name="BookOpenText" :size="13" />
|
|
303
|
+
{{ readingTime(note) }} {{ T.reading }}
|
|
304
|
+
</span>
|
|
305
|
+
</span>
|
|
306
|
+
</span>
|
|
307
|
+
</button>
|
|
308
|
+
|
|
309
|
+
<button
|
|
310
|
+
type="button"
|
|
311
|
+
class="rm-notes__caret"
|
|
312
|
+
:aria-expanded="open.includes(note.id)"
|
|
313
|
+
:aria-controls="`rm-note-${note.id}`"
|
|
314
|
+
:aria-label="open.includes(note.id) ? T.collapse : T.expand"
|
|
315
|
+
:title="open.includes(note.id) ? T.collapse : T.expand"
|
|
316
|
+
@click="toggle(note.id)"
|
|
317
|
+
>
|
|
318
|
+
<RmIcon :name="open.includes(note.id) ? 'CaretUp' : 'CaretDown'" :size="14" />
|
|
319
|
+
</button>
|
|
320
|
+
</header>
|
|
321
|
+
|
|
322
|
+
<p class="rm-notes__summary">{{ note.summary }}</p>
|
|
323
|
+
|
|
324
|
+
<div v-if="open.includes(note.id)" :id="`rm-note-${note.id}`" class="rm-notes__body">
|
|
325
|
+
<section v-for="(section, index) in note.sections" :key="index" class="rm-notes__section">
|
|
326
|
+
<p v-if="section.title" class="rm-notes__eyebrow">{{ section.title }}</p>
|
|
327
|
+
|
|
328
|
+
<template v-for="(block, blockIndex) in section.blocks" :key="blockIndex">
|
|
329
|
+
<p v-if="block.type === 'text'" class="rm-notes__p">
|
|
330
|
+
<span v-for="part in inline(block.text)" :key="part.key">
|
|
331
|
+
<b v-if="part.bold">{{ part.text }}</b>
|
|
332
|
+
<template v-else>{{ part.text }}</template>
|
|
333
|
+
</span>
|
|
334
|
+
</p>
|
|
335
|
+
|
|
336
|
+
<ul v-else-if="block.type === 'list'" class="rm-notes__ul">
|
|
337
|
+
<li v-for="(item, itemIndex) in block.items" :key="itemIndex">
|
|
338
|
+
<span v-for="part in inline(item)" :key="part.key">
|
|
339
|
+
<b v-if="part.bold">{{ part.text }}</b>
|
|
340
|
+
<template v-else>{{ part.text }}</template>
|
|
341
|
+
</span>
|
|
342
|
+
</li>
|
|
343
|
+
</ul>
|
|
344
|
+
|
|
345
|
+
<ul v-else-if="block.type === 'terms'" class="rm-notes__ul rm-notes__ul--terms">
|
|
346
|
+
<li v-for="(item, itemIndex) in block.items" :key="itemIndex">
|
|
347
|
+
<b>{{ item.term }}</b><template v-if="item.def"> : {{ item.def }}</template>
|
|
348
|
+
</li>
|
|
349
|
+
</ul>
|
|
350
|
+
|
|
351
|
+
<RmCallout v-else-if="block.type === 'callout'" :tone="block.tone ?? 'info'">
|
|
352
|
+
<b v-if="block.title">{{ block.title }} </b>
|
|
353
|
+
<span v-for="part in inline(block.text)" :key="part.key">
|
|
354
|
+
<b v-if="part.bold">{{ part.text }}</b>
|
|
355
|
+
<template v-else>{{ part.text }}</template>
|
|
356
|
+
</span>
|
|
357
|
+
</RmCallout>
|
|
358
|
+
|
|
359
|
+
<p v-else-if="block.type === 'example'" class="rm-notes__example">{{ block.text }}</p>
|
|
360
|
+
|
|
361
|
+
<a
|
|
362
|
+
v-else-if="block.type === 'link'"
|
|
363
|
+
class="rm-notes__link"
|
|
364
|
+
:href="block.to"
|
|
365
|
+
@click.prevent="onLink(block.to)"
|
|
366
|
+
>
|
|
367
|
+
<RmIcon name="ArrowUpRight" :size="14" />
|
|
368
|
+
<span>{{ block.label }}</span>
|
|
369
|
+
</a>
|
|
370
|
+
</template>
|
|
371
|
+
</section>
|
|
372
|
+
</div>
|
|
373
|
+
</article>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
</template>
|
|
377
|
+
|
|
378
|
+
<style scoped>
|
|
379
|
+
.rm-notes{align-self:stretch;flex-direction:column;font-family:var(--font-sans)}.rm-notes,.rm-notes__head{display:flex;gap:var(--space-12)}.rm-notes__head{align-items:center;justify-content:space-between}.rm-notes__title{color:var(--text-primary);font-family:var(--font-serif);font-size:var(--font-22);font-weight:var(--weight-bold);margin:0}.rm-notes__badge{align-items:center;background:var(--bg-hover);border-radius:var(--radius-pill);color:var(--color-secondary);display:inline-flex;font-size:var(--font-10);font-weight:var(--weight-bold);gap:var(--space-4);letter-spacing:.06em;padding:4px 10px;text-transform:uppercase}.rm-notes__desc{color:var(--text-secondary);font-size:var(--font-13);line-height:1.5;margin:0}.rm-notes__search{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md);color:var(--text-muted);display:flex;gap:var(--space-8);padding:var(--space-8) var(--space-12)}.rm-notes__search:focus-within{border-color:var(--color-secondary);box-shadow:0 0 0 3px rgba(var(--brand-rgb),.1)}.rm-notes__search input{background:none;border:none;color:var(--text-primary);flex:1;font-family:inherit;font-size:var(--font-13);min-width:0;outline:none}.rm-notes__search input::-moz-placeholder{color:var(--text-muted)}.rm-notes__search input::placeholder{color:var(--text-muted)}.rm-notes__chips{display:flex;flex-wrap:wrap;gap:var(--space-6)}.rm-notes__chip{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-pill);color:var(--text-secondary);cursor:pointer;display:inline-flex;font-family:inherit;font-size:var(--font-12);gap:var(--space-4);padding:5px 10px}.rm-notes__chip:hover{background:var(--bg-surface-3)}.rm-notes__chip--on{background:var(--bg-hover);border-color:var(--color-violet-pale);color:var(--color-primary);font-weight:var(--weight-semibold)}.rm-notes__chip-count{font-variant-numeric:tabular-nums;opacity:.8}.rm-notes__list,.rm-notes__note{display:flex;flex-direction:column;gap:var(--space-8)}.rm-notes__note{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-xl);padding:var(--space-16);transition:border-color var(--duration-fast) var(--ease-out)}.rm-notes__note:hover{border-color:var(--color-violet-pale)}.rm-notes__note--open{border-color:var(--color-secondary)}.rm-notes__note-head,.rm-notes__note-id{align-items:flex-start;display:flex;gap:var(--space-12)}.rm-notes__note-id{background:none;border:none;cursor:pointer;flex:1;font-family:inherit;min-width:0;padding:0;text-align:left}.rm-notes__note-icon{align-items:center;border-radius:var(--radius-lg);display:flex;flex-shrink:0;height:36px;justify-content:center;width:36px}.rm-notes__note-icon--primary{background:var(--bg-hover);color:var(--color-secondary)}.rm-notes__note-icon--info{background:var(--color-info-bg);color:var(--color-info)}.rm-notes__note-icon--warning{background:var(--color-warning-bg);color:var(--color-warning)}.rm-notes__note-icon--success{background:var(--color-success-bg);color:var(--color-success)}.rm-notes__note-icon--neutral{background:var(--bg-surface-2);color:var(--text-secondary)}.rm-notes__note-text{display:flex;flex-direction:column;gap:var(--space-6);min-width:0}.rm-notes__note-title{color:var(--text-primary);font-family:var(--font-serif);font-size:var(--font-16);font-weight:var(--weight-bold)}.rm-notes__note-meta{align-items:center;color:var(--text-muted);display:flex;flex-wrap:wrap;font-size:var(--font-11);gap:var(--space-8)}.rm-notes__cell{align-items:center;display:inline-flex;gap:var(--space-4)}.rm-notes__tag{border-radius:var(--radius-pill);font-size:var(--font-10);font-weight:var(--weight-bold);letter-spacing:.04em;padding:2px var(--space-6);text-transform:uppercase}.rm-notes__tag--primary{background:var(--bg-hover);color:var(--color-secondary)}.rm-notes__tag--info{background:var(--color-info-bg);color:var(--color-info)}.rm-notes__tag--warning{background:var(--color-warning-bg);color:var(--color-warning)}.rm-notes__tag--success{background:var(--color-success-bg);color:var(--color-success)}.rm-notes__tag--neutral{background:var(--bg-surface-2);color:var(--text-secondary)}.rm-notes__caret{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md);color:var(--text-secondary);cursor:pointer;display:flex;flex-shrink:0;height:28px;justify-content:center;width:28px}.rm-notes__caret:hover{background:var(--bg-surface-3)}.rm-notes__summary{color:var(--text-secondary);font-size:var(--font-13);line-height:1.6;margin:0}.rm-notes__body{border-top:1px solid var(--border-soft);display:flex;flex-direction:column;gap:var(--space-16);padding-top:var(--space-12)}.rm-notes__section{display:flex;flex-direction:column;gap:var(--space-8)}.rm-notes__eyebrow{color:var(--text-muted);font-size:var(--font-10);font-weight:var(--weight-bold);letter-spacing:.08em;margin:0;text-transform:uppercase}.rm-notes__p,.rm-notes__ul{color:var(--text-secondary);font-size:var(--font-13);line-height:1.65;margin:0}.rm-notes__ul{padding-left:var(--space-20)}.rm-notes__ul--terms{list-style:none;padding-left:0}.rm-notes__p b,.rm-notes__ul b{color:var(--text-primary)}.rm-notes__example{background:var(--bg-surface-3);border-left:3px solid var(--color-primary);border-radius:var(--radius-sm);color:var(--text-secondary);font-family:var(--font-mono);font-size:var(--font-12);margin:0;padding:var(--space-8) var(--space-12)}.rm-notes__link{align-items:center;color:var(--color-primary);cursor:pointer;display:inline-flex;font-size:var(--font-13);font-weight:var(--weight-semibold);gap:var(--space-4);text-decoration:underline;text-underline-offset:3px}
|
|
380
|
+
</style>
|