@docsector/docsector-reader 3.3.1 → 3.5.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.
@@ -0,0 +1,365 @@
1
+ const DEFAULT_EMBED = {
2
+ mode: 'link',
3
+ provider: 'link',
4
+ kind: 'link',
5
+ icon: 'link',
6
+ title: '',
7
+ providerLabel: '',
8
+ canonicalUrl: '',
9
+ displayUrl: '',
10
+ embedSrc: '',
11
+ aspectRatio: '',
12
+ frameHeight: 0,
13
+ allow: '',
14
+ allowFullscreen: false
15
+ }
16
+
17
+ const YOUTUBE_HOSTS = new Set([
18
+ 'youtu.be',
19
+ 'youtube.com',
20
+ 'www.youtube.com',
21
+ 'm.youtube.com',
22
+ 'music.youtube.com',
23
+ 'youtube-nocookie.com',
24
+ 'www.youtube-nocookie.com'
25
+ ])
26
+
27
+ const VIMEO_HOSTS = new Set([
28
+ 'vimeo.com',
29
+ 'www.vimeo.com',
30
+ 'player.vimeo.com'
31
+ ])
32
+
33
+ const SPOTIFY_HOSTS = new Set([
34
+ 'spotify.com',
35
+ 'www.spotify.com',
36
+ 'open.spotify.com'
37
+ ])
38
+
39
+ const CODEPEN_HOSTS = new Set([
40
+ 'codepen.io',
41
+ 'www.codepen.io'
42
+ ])
43
+
44
+ const SPOTIFY_PROVIDER_BY_KIND = {
45
+ track: {
46
+ kind: 'audio',
47
+ icon: 'music_note',
48
+ title: 'Spotify track',
49
+ frameHeight: 152
50
+ },
51
+ episode: {
52
+ kind: 'audio',
53
+ icon: 'podcasts',
54
+ title: 'Spotify episode',
55
+ frameHeight: 232
56
+ },
57
+ artist: {
58
+ kind: 'music',
59
+ icon: 'person',
60
+ title: 'Spotify artist',
61
+ frameHeight: 352
62
+ },
63
+ album: {
64
+ kind: 'music',
65
+ icon: 'album',
66
+ title: 'Spotify album',
67
+ frameHeight: 352
68
+ },
69
+ playlist: {
70
+ kind: 'music',
71
+ icon: 'queue_music',
72
+ title: 'Spotify playlist',
73
+ frameHeight: 352
74
+ },
75
+ show: {
76
+ kind: 'podcast',
77
+ icon: 'radio',
78
+ title: 'Spotify show',
79
+ frameHeight: 352
80
+ }
81
+ }
82
+
83
+ const createFallbackResult = (rawUrl = '', title = '') => {
84
+ const trimmed = String(rawUrl || '').trim()
85
+ const normalized = parseHttpUrl(trimmed)
86
+
87
+ return {
88
+ ...DEFAULT_EMBED,
89
+ title: title || normalized?.hostname || trimmed,
90
+ providerLabel: normalized?.hostname || '',
91
+ canonicalUrl: normalized?.toString() || trimmed,
92
+ displayUrl: normalized?.toString() || trimmed
93
+ }
94
+ }
95
+
96
+ const parseHttpUrl = (rawUrl = '') => {
97
+ const trimmed = String(rawUrl || '').trim()
98
+
99
+ if (!trimmed) {
100
+ return null
101
+ }
102
+
103
+ try {
104
+ const url = new URL(trimmed)
105
+
106
+ if (!['http:', 'https:'].includes(url.protocol)) {
107
+ return null
108
+ }
109
+
110
+ return url
111
+ } catch {
112
+ return null
113
+ }
114
+ }
115
+
116
+ const mergeSearchParams = (url, options = {}) => {
117
+ const params = new URLSearchParams(url.search)
118
+
119
+ ;(options.remove || []).forEach((key) => {
120
+ params.delete(key)
121
+ })
122
+
123
+ Object.entries(options.set || {}).forEach(([key, value]) => {
124
+ if (value === '' || value === null || value === undefined) {
125
+ params.delete(key)
126
+ return
127
+ }
128
+
129
+ params.set(key, String(value))
130
+ })
131
+
132
+ const query = params.toString()
133
+ return query ? `?${query}` : ''
134
+ }
135
+
136
+ const parseTimeToSeconds = (value = '') => {
137
+ const trimmed = String(value || '').trim()
138
+
139
+ if (!trimmed) {
140
+ return ''
141
+ }
142
+
143
+ if (/^\d+$/.test(trimmed)) {
144
+ return trimmed
145
+ }
146
+
147
+ const match = trimmed.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/i)
148
+ if (!match) {
149
+ return ''
150
+ }
151
+
152
+ const hours = Number(match[1] || 0)
153
+ const minutes = Number(match[2] || 0)
154
+ const seconds = Number(match[3] || 0)
155
+ const total = (hours * 3600) + (minutes * 60) + seconds
156
+
157
+ return total > 0 ? String(total) : ''
158
+ }
159
+
160
+ const resolveYouTubeVideoId = (url) => {
161
+ const segments = url.pathname.split('/').filter(Boolean)
162
+
163
+ if (url.hostname === 'youtu.be') {
164
+ return segments[0] || ''
165
+ }
166
+
167
+ if (segments[0] === 'watch') {
168
+ return url.searchParams.get('v') || ''
169
+ }
170
+
171
+ if (['embed', 'shorts', 'live', 'v'].includes(segments[0])) {
172
+ return segments[1] || ''
173
+ }
174
+
175
+ return url.searchParams.get('v') || ''
176
+ }
177
+
178
+ const resolveYouTubeEmbed = (url, title = '') => {
179
+ const videoId = resolveYouTubeVideoId(url)
180
+
181
+ if (!videoId) {
182
+ return null
183
+ }
184
+
185
+ const start = parseTimeToSeconds(url.searchParams.get('t'))
186
+ const query = mergeSearchParams(url, {
187
+ remove: ['v', 't'],
188
+ set: {
189
+ ...(start ? { start } : {}),
190
+ ...((url.searchParams.get('loop') === '1' || url.searchParams.get('loop') === 'true') && !url.searchParams.get('playlist')
191
+ ? { playlist: videoId }
192
+ : {})
193
+ }
194
+ })
195
+
196
+ return {
197
+ ...DEFAULT_EMBED,
198
+ mode: 'embed',
199
+ provider: 'youtube',
200
+ kind: 'video',
201
+ icon: 'smart_display',
202
+ title: title || 'YouTube video',
203
+ providerLabel: 'YouTube',
204
+ canonicalUrl: url.toString(),
205
+ displayUrl: url.toString(),
206
+ embedSrc: `https://www.youtube.com/embed/${videoId}${query}`,
207
+ aspectRatio: '16 / 9',
208
+ allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share',
209
+ allowFullscreen: true
210
+ }
211
+ }
212
+
213
+ const resolveVimeoVideoId = (url) => {
214
+ const segments = url.pathname.split('/').filter(Boolean)
215
+
216
+ if (segments[0] === 'video') {
217
+ return segments[1] || ''
218
+ }
219
+
220
+ return segments.find((segment) => /^\d+$/.test(segment)) || ''
221
+ }
222
+
223
+ const resolveVimeoEmbed = (url, title = '') => {
224
+ const videoId = resolveVimeoVideoId(url)
225
+
226
+ if (!videoId) {
227
+ return null
228
+ }
229
+
230
+ return {
231
+ ...DEFAULT_EMBED,
232
+ mode: 'embed',
233
+ provider: 'vimeo',
234
+ kind: 'video',
235
+ icon: 'movie',
236
+ title: title || 'Vimeo video',
237
+ providerLabel: 'Vimeo',
238
+ canonicalUrl: url.toString(),
239
+ displayUrl: url.toString(),
240
+ embedSrc: `https://player.vimeo.com/video/${videoId}${mergeSearchParams(url)}`,
241
+ aspectRatio: '16 / 9',
242
+ allow: 'autoplay; fullscreen; picture-in-picture',
243
+ allowFullscreen: true
244
+ }
245
+ }
246
+
247
+ const resolveSpotifyParts = (url) => {
248
+ const segments = url.pathname.split('/').filter(Boolean)
249
+ const embedIndex = segments[0] === 'embed' ? 1 : 0
250
+ const type = segments[embedIndex] || ''
251
+ const id = segments[embedIndex + 1] || ''
252
+
253
+ if (!SPOTIFY_PROVIDER_BY_KIND[type] || !id) {
254
+ return null
255
+ }
256
+
257
+ return { type, id }
258
+ }
259
+
260
+ const resolveSpotifyEmbed = (url, title = '') => {
261
+ const parts = resolveSpotifyParts(url)
262
+
263
+ if (parts === null) {
264
+ return null
265
+ }
266
+
267
+ const definition = SPOTIFY_PROVIDER_BY_KIND[parts.type]
268
+
269
+ return {
270
+ ...DEFAULT_EMBED,
271
+ mode: 'embed',
272
+ provider: 'spotify',
273
+ kind: definition.kind,
274
+ icon: definition.icon,
275
+ title: title || definition.title,
276
+ providerLabel: 'Spotify',
277
+ canonicalUrl: url.toString(),
278
+ displayUrl: url.toString(),
279
+ embedSrc: `https://open.spotify.com/embed/${parts.type}/${parts.id}${mergeSearchParams(url)}`,
280
+ aspectRatio: '',
281
+ frameHeight: definition.frameHeight,
282
+ allow: 'autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture',
283
+ allowFullscreen: false
284
+ }
285
+ }
286
+
287
+ const resolveCodePenParts = (url) => {
288
+ const segments = url.pathname.split('/').filter(Boolean)
289
+
290
+ if (segments[0] === 'team') {
291
+ const team = segments[1] || ''
292
+ const penType = segments[2] || ''
293
+ const penId = segments[3] || ''
294
+
295
+ if (!team || !penId || !['pen', 'full', 'details', 'embed', 'debug'].includes(penType)) {
296
+ return null
297
+ }
298
+
299
+ return {
300
+ user: `team/${team}`,
301
+ penId
302
+ }
303
+ }
304
+
305
+ const user = segments[0] || ''
306
+ const penType = segments[1] || ''
307
+ const penId = segments[2] || ''
308
+
309
+ if (!user || !penId || !['pen', 'full', 'details', 'embed', 'debug'].includes(penType)) {
310
+ return null
311
+ }
312
+
313
+ return { user, penId }
314
+ }
315
+
316
+ const resolveCodePenEmbed = (url, title = '') => {
317
+ const parts = resolveCodePenParts(url)
318
+
319
+ if (parts === null) {
320
+ return null
321
+ }
322
+
323
+ return {
324
+ ...DEFAULT_EMBED,
325
+ mode: 'embed',
326
+ provider: 'codepen',
327
+ kind: 'code',
328
+ icon: 'code',
329
+ title: title || 'CodePen embed',
330
+ providerLabel: 'CodePen',
331
+ canonicalUrl: url.toString(),
332
+ displayUrl: url.toString(),
333
+ embedSrc: `https://codepen.io/${parts.user}/embed/${parts.penId}${mergeSearchParams(url)}`,
334
+ aspectRatio: '16 / 9',
335
+ allow: 'accelerometer; camera; clipboard-write; display-capture; encrypted-media; geolocation; gyroscope; microphone; midi; web-share',
336
+ allowFullscreen: true
337
+ }
338
+ }
339
+
340
+ export const resolveEmbeddedUrl = (rawUrl = '', options = {}) => {
341
+ const title = String(options.title || '').trim()
342
+ const normalized = parseHttpUrl(rawUrl)
343
+
344
+ if (normalized === null) {
345
+ return createFallbackResult(rawUrl, title)
346
+ }
347
+
348
+ if (YOUTUBE_HOSTS.has(normalized.hostname)) {
349
+ return resolveYouTubeEmbed(normalized, title) || createFallbackResult(rawUrl, title)
350
+ }
351
+
352
+ if (VIMEO_HOSTS.has(normalized.hostname)) {
353
+ return resolveVimeoEmbed(normalized, title) || createFallbackResult(rawUrl, title)
354
+ }
355
+
356
+ if (SPOTIFY_HOSTS.has(normalized.hostname)) {
357
+ return resolveSpotifyEmbed(normalized, title) || createFallbackResult(rawUrl, title)
358
+ }
359
+
360
+ if (CODEPEN_HOSTS.has(normalized.hostname)) {
361
+ return resolveCodePenEmbed(normalized, title) || createFallbackResult(rawUrl, title)
362
+ }
363
+
364
+ return createFallbackResult(rawUrl, title)
365
+ }
package/src/css/app.sass CHANGED
@@ -15,6 +15,11 @@ body
15
15
  body.body--light
16
16
  --q-primary: #655529 !important
17
17
  --q-secondary: #000080 !important
18
+ --task-list-checkbox-bg: #ffffff
19
+ --task-list-checkbox-border: #7c6a3a
20
+ --task-list-checkbox-checked-bg: #5e4c1f
21
+ --task-list-checkbox-checked-border: #4f3f18
22
+ --task-list-checkbox-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2.3' d='M3.25 8.5 6.4 11.4 12.75 4.9'/%3E%3C/svg%3E")
18
23
 
19
24
  --q-primary-background: #FFF !important
20
25
 
@@ -48,6 +53,11 @@ body.body--dark
48
53
  --q-primary-in-dark-bg: #C1A667 !important
49
54
  --q-secondary: #FFA07A !important
50
55
  --q-negative: #fc001b !important
56
+ --task-list-checkbox-bg: #181818
57
+ --task-list-checkbox-border: #8f8a78
58
+ --task-list-checkbox-checked-bg: #d3b874
59
+ --task-list-checkbox-checked-border: #ebd08a
60
+ --task-list-checkbox-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23151515' stroke-linecap='round' stroke-linejoin='round' stroke-width='2.3' d='M3.25 8.5 6.4 11.4 12.75 4.9'/%3E%3C/svg%3E")
51
61
 
52
62
  --q-light-in-dark-1: #ababab
53
63
  --q-light-in-dark-2: #c9c9c9
@@ -199,6 +209,39 @@ body.body--dark
199
209
  overflow-y: hidden
200
210
  padding: 0.35rem 0.1rem
201
211
 
212
+ ul.contains-task-list,
213
+ ol.contains-task-list
214
+ list-style: none
215
+ padding-left: 1.75rem
216
+
217
+ li.task-list-item
218
+ list-style: none
219
+
220
+ input.task-list-item-checkbox
221
+ appearance: none
222
+ -webkit-appearance: none
223
+ width: 1rem
224
+ height: 1rem
225
+ margin: 0 0.55rem 0 -1.45rem
226
+ vertical-align: middle
227
+ pointer-events: none
228
+ opacity: 1
229
+ border: 2px solid var(--task-list-checkbox-border)
230
+ border-radius: 0.2rem
231
+ background-color: var(--task-list-checkbox-bg)
232
+ background-position: center
233
+ background-repeat: no-repeat
234
+ background-size: 0.72rem 0.72rem
235
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08)
236
+
237
+ &:checked
238
+ background-color: var(--task-list-checkbox-checked-bg)
239
+ border-color: var(--task-list-checkbox-checked-border)
240
+ background-image: var(--task-list-checkbox-check)
241
+
242
+ &:disabled
243
+ opacity: 1
244
+
202
245
  a
203
246
  text-decoration: none
204
247
  outline: 0
@@ -0,0 +1,33 @@
1
+ ## Overview
2
+
3
+ Embedded URLs turn supported public links into responsive embeds directly inside Markdown pages.
4
+
5
+ This block is a higher-level alternative to raw iframe markup when the source is a supported provider and the page should keep a consistent card + preview treatment.
6
+
7
+ ## Markdown Syntax
8
+
9
+ ```html
10
+ <d-embedded-url url="https://www.youtube.com/watch?v=M7lc1UVf-VE" title="YouTube player demo">
11
+ Optional caption rendered as inline Markdown.
12
+ </d-embedded-url>
13
+ ```
14
+
15
+ You can also omit the caption body when the provider preview already gives enough context:
16
+
17
+ ```html
18
+ <d-embedded-url url="https://open.spotify.com/track/7ouMYWpwJ422jRcDASZB7P" />
19
+ ```
20
+
21
+ ## Supported Providers
22
+
23
+ - YouTube
24
+ - Vimeo
25
+ - Spotify
26
+ - CodePen
27
+
28
+ ## Notes
29
+
30
+ - `url` is required. `title` is optional.
31
+ - The block preserves the original query string, so provider options such as `autoplay=1&loop=1` continue to work when the provider supports them.
32
+ - Unsupported or private URLs fall back to a safe external-link card instead of attempting a generic iframe.
33
+ - Use Raw HTML when you need a provider outside the curated list or when you need full manual iframe control.
@@ -0,0 +1,33 @@
1
+ ## Apresentação
2
+
3
+ URLs embutidas transformam links públicos suportados em embeds responsivos diretamente dentro das páginas Markdown.
4
+
5
+ Este block é uma alternativa de nível mais alto ao uso manual de iframe quando a origem pertence a um provider suportado e a página deve manter um tratamento consistente de preview + card.
6
+
7
+ ## Sintaxe Markdown
8
+
9
+ ```html
10
+ <d-embedded-url url="https://www.youtube.com/watch?v=M7lc1UVf-VE" title="Demo do player do YouTube">
11
+ Legenda opcional renderizada como Markdown inline.
12
+ </d-embedded-url>
13
+ ```
14
+
15
+ Você também pode omitir o corpo quando o preview do provider já entrega contexto suficiente:
16
+
17
+ ```html
18
+ <d-embedded-url url="https://open.spotify.com/track/7ouMYWpwJ422jRcDASZB7P" />
19
+ ```
20
+
21
+ ## Providers suportados
22
+
23
+ - YouTube
24
+ - Vimeo
25
+ - Spotify
26
+ - CodePen
27
+
28
+ ## Observações
29
+
30
+ - `url` é obrigatório. `title` é opcional.
31
+ - O block preserva a query string original, então opções do provider como `autoplay=1&loop=1` continuam funcionando quando houver suporte.
32
+ - URLs não suportadas ou privadas fazem fallback para um card seguro com link externo, sem tentar um iframe genérico.
33
+ - Use HTML bruto quando você precisar de um provider fora da lista curada ou de controle manual total sobre o iframe.
@@ -0,0 +1,25 @@
1
+ ## Showcase
2
+
3
+ ### YouTube Video
4
+
5
+ <d-embedded-url url="https://www.youtube.com/watch?v=M7lc1UVf-VE&autoplay=1&loop=1" title="YouTube player demo">
6
+ The original playback query parameters stay attached to the provider URL.
7
+ </d-embedded-url>
8
+
9
+ ### Spotify Track
10
+
11
+ <d-embedded-url url="https://open.spotify.com/track/7ouMYWpwJ422jRcDASZB7P?si=1234">
12
+ Spotify links render with the provider-specific compact player height.
13
+ </d-embedded-url>
14
+
15
+ ### CodePen Demo
16
+
17
+ <d-embedded-url url="https://codepen.io/team/codepen/pen/PNaGbb?default-tab=result" title="Interactive demo">
18
+ Use the same block for live front-end demos without dropping to raw HTML.
19
+ </d-embedded-url>
20
+
21
+ ### Unsupported URL Fallback
22
+
23
+ <d-embedded-url url="https://example.com/docs/embed-me" title="External API docs">
24
+ Unsupported providers render a safe link card so the reading flow still keeps the URL visible and actionable.
25
+ </d-embedded-url>
@@ -0,0 +1,25 @@
1
+ ## Demonstração
2
+
3
+ ### Vídeo do YouTube
4
+
5
+ <d-embedded-url url="https://www.youtube.com/watch?v=M7lc1UVf-VE&autoplay=1&loop=1" title="Demo do player do YouTube">
6
+ Os parâmetros originais de reprodução continuam anexados à URL do provider.
7
+ </d-embedded-url>
8
+
9
+ ### Faixa do Spotify
10
+
11
+ <d-embedded-url url="https://open.spotify.com/track/7ouMYWpwJ422jRcDASZB7P?si=1234">
12
+ Links do Spotify renderizam com a altura compacta específica do provider.
13
+ </d-embedded-url>
14
+
15
+ ### Demo no CodePen
16
+
17
+ <d-embedded-url url="https://codepen.io/team/codepen/pen/PNaGbb?default-tab=result" title="Demo interativa">
18
+ Use o mesmo block para demos front-end ao vivo sem cair para HTML bruto.
19
+ </d-embedded-url>
20
+
21
+ ### Fallback para URL não suportada
22
+
23
+ <d-embedded-url url="https://example.com/docs/embed-me" title="Documentação externa da API">
24
+ Providers não suportados renderizam um card seguro com link externo, mantendo a URL visível e acionável no fluxo de leitura.
25
+ </d-embedded-url>
@@ -0,0 +1,20 @@
1
+ ## Overview
2
+
3
+ Use task lists when each item should carry an explicit done or not-done state.
4
+
5
+ They work well for release preparation, editorial queues, migration follow-ups, and any checklist where progress matters as much as the content itself.
6
+
7
+ ## Markdown Syntax
8
+
9
+ ```markdown
10
+ - [ ] Write the overview page
11
+ - [x] Add localized examples
12
+ - [ ] Review screenshots
13
+ - [x] Update internal links
14
+ ```
15
+
16
+ ## Notes
17
+
18
+ - Task lists follow the same indentation rules as regular Markdown lists.
19
+ - Use `[ ]` for pending items and `[x]` for completed items.
20
+ - Published readers see static checkboxes and cannot toggle them from the page.
@@ -0,0 +1,20 @@
1
+ ## Visão Geral
2
+
3
+ Use listas de tarefas quando cada item precisar deixar explícito se foi concluído ou não.
4
+
5
+ Elas funcionam bem para preparação de releases, filas editoriais, acompanhamentos de migração e qualquer checklist em que o progresso importa tanto quanto o conteúdo.
6
+
7
+ ## Sintaxe em Markdown
8
+
9
+ ```markdown
10
+ - [ ] Escreva a página de visão geral
11
+ - [x] Adicione exemplos localizados
12
+ - [ ] Revise as capturas de tela
13
+ - [x] Atualize os links internos
14
+ ```
15
+
16
+ ## Observações
17
+
18
+ - Listas de tarefas seguem as mesmas regras de indentação das listas Markdown comuns.
19
+ - Use `[ ]` para itens pendentes e `[x]` para itens concluídos.
20
+ - Leitores da documentação publicada veem checkboxes estáticos e não podem alterná-los pela página.
@@ -0,0 +1,23 @@
1
+ ## Showcase
2
+
3
+ ### Simple Tasks
4
+
5
+ - [ ] Write the overview page
6
+ - [x] Add the showcase page
7
+ - [ ] Review internal links
8
+
9
+ ### Nested Tasks
10
+
11
+ - [ ] Prepare the release
12
+ - [x] Update the changelog
13
+ - [ ] Run smoke tests
14
+ - [ ] Publish the package
15
+
16
+ ### Mixed Guidance
17
+
18
+ > [!NOTE]
19
+ > Readers can see the authored state of each task, but published checkboxes stay read-only.
20
+
21
+ - [x] Keep completed items checked in the source.
22
+ - [ ] Use task lists when the state matters more than the order.
23
+ - Regular bullet lists are still better for collections that do not need a done/not-done signal.
@@ -0,0 +1,23 @@
1
+ ## Showcase
2
+
3
+ ### Tarefas Simples
4
+
5
+ - [ ] Escreva a página de visão geral
6
+ - [x] Adicione a página de showcase
7
+ - [ ] Revise os links internos
8
+
9
+ ### Tarefas Aninhadas
10
+
11
+ - [ ] Prepare a release
12
+ - [x] Atualize o changelog
13
+ - [ ] Execute smoke tests
14
+ - [ ] Publique o pacote
15
+
16
+ ### Orientação de Uso
17
+
18
+ > [!NOTE]
19
+ > Leitores conseguem ver o estado definido no Markdown, mas os checkboxes publicados continuam somente leitura.
20
+
21
+ - [x] Mantenha marcados os itens concluídos no conteúdo-fonte.
22
+ - [ ] Use listas de tarefas quando o estado importar mais do que a ordem.
23
+ - Listas com marcadores continuam melhores para coleções que não precisam de sinal de concluído ou pendente.