umberto 10.2.0 → 10.4.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +16 -14
  2. package/package.json +1 -1
  3. package/src/api-builder/classes/doc-data-factory.js +10 -2
  4. package/src/data-converter/converters/typedoc/abstractparser.js +14 -0
  5. package/src/data-converter/converters/typedoc/accessorparser.js +1 -0
  6. package/src/data-converter/converters/typedoc/classparser.js +1 -0
  7. package/src/data-converter/converters/typedoc/constantparser.js +1 -0
  8. package/src/data-converter/converters/typedoc/eventparser.js +1 -0
  9. package/src/data-converter/converters/typedoc/functionparser.js +1 -0
  10. package/src/data-converter/converters/typedoc/interfaceparser.js +1 -0
  11. package/src/data-converter/converters/typedoc/methodparser.js +1 -0
  12. package/src/data-converter/converters/typedoc/propertyparser.js +1 -0
  13. package/src/data-converter/converters/typedoc/typeparser.js +1 -0
  14. package/src/helpers/create-filtering-data-attribs.js +4 -0
  15. package/src/tasks/build-documentation.js +6 -0
  16. package/themes/umberto/layout/gloria/_api-docs/_header/index.pug +2 -0
  17. package/themes/umberto/layout/gloria/_api-docs/_mixin/_badge-tag.pug +6 -0
  18. package/themes/umberto/layout/gloria/_api-docs/_mixin/_class-item.pug +2 -4
  19. package/themes/umberto/layout/gloria/_api-docs/_mixin/_method.pug +2 -4
  20. package/themes/umberto/layout/gloria/_api-docs/_mixin/_property.pug +2 -5
  21. package/themes/umberto/layout/gloria/_api-docs/_partial/filter.pug +4 -0
  22. package/themes/umberto/layout/gloria/_api-docs/_subheader/_style.scss +7 -0
  23. package/themes/umberto/layout/gloria/_api-docs/_subheader/index.pug +4 -0
  24. package/themes/umberto/layout/gloria/_components/dropdown/index.pug +5 -2
  25. package/themes/umberto/layout/gloria/_components/tag/_style.scss +12 -0
  26. package/themes/umberto/layout/gloria/_modules/header/nav-project-select-dropdown.pug +46 -3
  27. package/themes/umberto/layout/gloria/_modules/mobile-nav/index.pug +40 -5
  28. package/themes/umberto/layout/gloria/_modules/mobile-nav/slide-parts.pug +7 -2
  29. package/themes/umberto/layout/gloria/theme.pug +2 -0
  30. package/themes/umberto/layout/umberto/_api-docs/_partial/filter.pug +4 -0
  31. package/themes/umberto/src/gloria/js/modules/api-filter.js +19 -10
  32. package/themes/umberto/src/umberto/css/_badge.scss +1 -0
  33. package/themes/umberto/src/umberto/css/helpers/_color.scss +1 -0
  34. package/themes/umberto/src/umberto/js/_filtering.js +1 -0
package/CHANGELOG.md CHANGED
@@ -1,6 +1,22 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
+ ## [10.4.0](https://github.com/cksource/umberto/compare/v10.3.0...v10.4.0) (April 9, 2026)
5
+
6
+ ### Features
7
+
8
+ * Added the LTS edition to the docs product picker.
9
+
10
+
11
+ ## [10.3.0](https://github.com/cksource/umberto/compare/v10.2.0...v10.3.0) (April 3, 2026)
12
+
13
+ ### Features
14
+
15
+ * Added support for displaying and filtering experimental API items in TypeDoc-based API docs.
16
+
17
+ Experimental annotations now appear in page headers and member listings, and can be toggled with the API filters.
18
+
19
+
4
20
  ## [10.2.0](https://github.com/cksource/umberto/compare/v10.1.4...v10.2.0) (April 2, 2026)
5
21
 
6
22
  ### Features
@@ -31,20 +47,6 @@ Changelog
31
47
 
32
48
  * Fix regression introduced in `v10.1.2` affecting slugs generated for headings. Slugs generated now should be the same as in `v10.1.1` and previous versions of Umberto.
33
49
 
34
-
35
- ## [10.1.2](https://github.com/cksource/umberto/compare/v10.1.1...v10.1.2) (March 9, 2026)
36
-
37
- ### Bug fixes
38
-
39
- * Removed unmaintained `hexo-renderer-markdown-it-plus` and `@ckeditor/jsdoc-plugins` dependencies by replacing them with maintained and local implementations, resolving the reported `markdown-it`-related vulnerabilities in Umberto's dependency tree.
40
-
41
-
42
- ## [10.1.1](https://github.com/cksource/umberto/compare/v10.1.0...v10.1.1) (March 4, 2026)
43
-
44
- ### Bug fixes
45
-
46
- * Fixed a deprecation warning displayed during documentation builds by migrating Sass compilation to the modern Dart Sass JavaScript API. The `legacy-js-api` warning is no longer emitted.
47
-
48
50
  ---
49
51
 
50
52
  To see all releases, visit the [release page](https://github.com/cksource/umberto/releases).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "umberto",
3
- "version": "10.2.0",
3
+ "version": "10.4.0",
4
4
  "description": "CKSource Documentation builder",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -62,6 +62,7 @@ export class DocDataFactory {
62
62
  access: doclet.access,
63
63
  inherited: doclet.mixed ? false : doclet.inherited,
64
64
  mixed: doclet.mixed,
65
+ experimental: doclet.experimental || false,
65
66
  deprecated: doclet.deprecated,
66
67
  since: doclet.since,
67
68
  file: doclet.file,
@@ -264,6 +265,7 @@ export class DocDataFactory {
264
265
  */
265
266
  getClassFull( doclet ) {
266
267
  const classMembersCollection = this._dataCollection.get( `memberof:${ doclet.longname }` );
268
+ const base = this._getBase( doclet, { headingDecreaseLevel: 2 } );
267
269
  const configOptions = classMembersCollection.get( 'cfg' );
268
270
  const properties = classMembersCollection.get( 'member' );
269
271
  const methods = classMembersCollection.get( 'function' );
@@ -328,7 +330,7 @@ export class DocDataFactory {
328
330
  augmentsNested.unshift( doclet.longname );
329
331
  augmentsNested.reverse();
330
332
 
331
- return Object.assign( this._getBase( doclet, { headingDecreaseLevel: 2 } ), {
333
+ return Object.assign( base, {
332
334
  configOptions: configOptionsProcessed,
333
335
  properties: propertiesProcessed,
334
336
  staticProperties,
@@ -644,6 +646,13 @@ function prepareBadges( item, context ) {
644
646
  badges.push( deprecatedBadge );
645
647
  }
646
648
 
649
+ if ( item.experimental ) {
650
+ badges.push( {
651
+ class: 'cyan badge--experimental',
652
+ text: 'experimental'
653
+ } );
654
+ }
655
+
647
656
  if ( item.chainable ) {
648
657
  badges.push( {
649
658
  class: 'green',
@@ -724,7 +733,6 @@ function prepareBadges( item, context ) {
724
733
 
725
734
  return badges;
726
735
  }
727
-
728
736
  function sortLongnamesAlphabetically( arr ) {
729
737
  if ( !arr || !arr.length ) {
730
738
  return;
@@ -370,6 +370,20 @@ export class AbstractParser {
370
370
  return false;
371
371
  }
372
372
 
373
+ /**
374
+ * Checks if the member is marked as `experimental` one.
375
+ *
376
+ * @param {BaseReflection} item
377
+ * @returns {Boolean}
378
+ */
379
+ isExperimental( item ) {
380
+ if ( item.comment && item.comment.modifierTags ) {
381
+ return item.comment.modifierTags.some( tag => tag === '@experimental' );
382
+ }
383
+
384
+ return false;
385
+ }
386
+
373
387
  /**
374
388
  * Checks if the member is marked by the `@deprecated` JSDoc tag.
375
389
  *
@@ -53,6 +53,7 @@ export class AccessorParser extends AbstractParser {
53
53
  description: this.getComment( getSignature || setSignature ),
54
54
  file: this.getFile( item ),
55
55
  skipSource: this.shouldSkipSource( getSignature || setSignature ),
56
+ experimental: this.isExperimental( getSignature || setSignature ),
56
57
  deprecated: this.isDeprecated( getSignature || setSignature ),
57
58
  inherited: !!item.inheritedFrom,
58
59
 
@@ -37,6 +37,7 @@ export class ClassParser extends AbstractParser {
37
37
  description: this.getComment( item ),
38
38
  file: this.getFile( item ),
39
39
  skipSource: this.shouldSkipSource( item ),
40
+ experimental: this.isExperimental( item ),
40
41
  deprecated: this.isDeprecated( item ),
41
42
 
42
43
  // Properties specified below will be mapped to module names once the entire project is transformed.
@@ -35,6 +35,7 @@ export class ConstantParser extends AbstractParser {
35
35
  extraId: `constant-${ item.name }`,
36
36
  file: this.getFile( item ),
37
37
  skipSource: this.shouldSkipSource( item ),
38
+ experimental: this.isExperimental( item ),
38
39
  description: this.getComment( item ),
39
40
  deprecated: this.isDeprecated( item ),
40
41
 
@@ -38,6 +38,7 @@ export class EventParser extends AbstractParser {
38
38
  description: this.getComment( item ),
39
39
  file: this.getFile( item ),
40
40
  skipSource: this.shouldSkipSource( item ),
41
+ experimental: this.isExperimental( item ),
41
42
  deprecated: this.isDeprecated( item ),
42
43
  inherited: !!item.inheritedFrom,
43
44
  see: this.getRelated( item, parentName ),
@@ -38,6 +38,7 @@ export class FunctionParser extends AbstractParser {
38
38
  description: this.getComment( signature ),
39
39
  file: this.getFile( item, index ),
40
40
  skipSource: this.shouldSkipSource( signature ),
41
+ experimental: this.isExperimental( signature ),
41
42
  deprecated: this.isDeprecated( signature ),
42
43
  see: this.getRelated( signature, parentName ),
43
44
 
@@ -36,6 +36,7 @@ export class InterfaceParser extends AbstractParser {
36
36
  description: this.getComment( item ),
37
37
  file: this.getFile( item ),
38
38
  skipSource: this.shouldSkipSource( item ),
39
+ experimental: this.isExperimental( item ),
39
40
  deprecated: this.isDeprecated( item ),
40
41
 
41
42
  typeParameters: null,
@@ -39,6 +39,7 @@ export class MethodParser extends AbstractParser {
39
39
  description: this.getComment( signature ),
40
40
  file: this.getFile( item, index ),
41
41
  skipSource: this.shouldSkipSource( signature ),
42
+ experimental: this.isExperimental( signature ),
42
43
  deprecated: this.isDeprecated( signature ),
43
44
  inherited: !!signature.inheritedFrom,
44
45
  see: this.getRelated( signature, parentName ),
@@ -40,6 +40,7 @@ export class PropertyParser extends AbstractParser {
40
40
  description: this.getComment( item ),
41
41
  file: this.getFile( item ),
42
42
  skipSource: this.shouldSkipSource( item ),
43
+ experimental: this.isExperimental( item ),
43
44
  deprecated: this.isDeprecated( item ),
44
45
  inherited: !!item.inheritedFrom,
45
46
  see: this.getRelated( item, parentName ),
@@ -32,6 +32,7 @@ export class TypeParser extends AbstractParser {
32
32
  extraId: this.getExtraId( item ),
33
33
  file: this.getFile( item ),
34
34
  skipSource: this.shouldSkipSource( item ),
35
+ experimental: this.isExperimental( item ),
35
36
  description: this.getComment( item ),
36
37
  deprecated: this.isDeprecated( item ),
37
38
  firedBy: this.getFiredBy( item, parentName ),
@@ -28,5 +28,9 @@ export const createFilteringDataAttribs = item => {
28
28
  attrData[ 'data-deprecated' ] = 'true';
29
29
  }
30
30
 
31
+ if ( item.experimental ) {
32
+ attrData[ 'data-experimental' ] = 'true';
33
+ }
34
+
31
35
  return attrData;
32
36
  };
@@ -161,6 +161,12 @@ export const buildDocumentation = options => {
161
161
  macrosVariables: mainConfig.variables,
162
162
  quickNavigationProjects: mainConfig.quickNavigationProjects || [
163
163
  { name: 'CKEditor 5', slug: 'ckeditor5' },
164
+ {
165
+ name: 'CKEditor 5 LTS Edition',
166
+ slug: 'ckeditor5',
167
+ channel: 'lts-v47',
168
+ rel: 'nofollow'
169
+ },
164
170
  { name: 'Cloud Services', slug: 'cs' },
165
171
  { name: 'CKBox', slug: 'ckbox' }
166
172
  ],
@@ -37,6 +37,8 @@ mixin apiHeader( config )
37
37
  +checkbox({ id:'f7', name: 'name4', className: 'js-api-filter-checkbox', label: 'Internal', value: 'internal', expanded: true })
38
38
  +dropdown-item({ narrow: true })
39
39
  +checkbox({ id:'f5', name: 'name7', className: 'js-api-filter-checkbox', label: 'Deprecated', value: 'deprecated', expanded: true, checked: true })
40
+ +dropdown-item({ narrow: true })
41
+ +checkbox({ id:'f8', name: 'name8', className: 'js-api-filter-checkbox', label: 'Experimental', value: 'experimental', expanded: true, checked: true })
40
42
 
41
43
  +button({
42
44
  icon: 'unordered-list',
@@ -0,0 +1,6 @@
1
+ mixin badgeTag( badge, options = {} )
2
+ - const iconNameByBadgeText = { private: 'lock', observable: 'eye', experimental: 'experiment' };
3
+ - const iconName = iconNameByBadgeText[ badge.text ] || null;
4
+ - const linkTo = badge && badge.linkTo ? badge.linkTo : false;
5
+ - const className = [ options.className || '', linkTo ? 'tag--link' : '', badge.text == 'experimental' ? 'tag--experimental' : '' ].filter( Boolean ).join( ' ' );
6
+ +tag({ label: badge.text, icon: iconName, iconSize: options.iconSize || 'base', href: linkTo, className })
@@ -1,5 +1,6 @@
1
1
  include ./_link-or-text.pug
2
2
  include ./_badge.pug
3
+ include ./_badge-tag.pug
3
4
  include ./_dev-names.pug
4
5
  include ./_api-see-source.pug
5
6
  include ../../_components/icon/index
@@ -22,10 +23,7 @@ mixin classItem( item )
22
23
  if isNonEmptyArray( item.badges )
23
24
  +tagWrapper()
24
25
  each badge in item.badges
25
- - const iconName = badge.text == 'private' ? 'lock' : badge.text == 'observable' ? 'eye' : null;
26
- - const linkTo = badge && badge.linkTo ? badge.linkTo : null;
27
- - const linkClassName = linkTo ? 'tag--link' : '';
28
- +tag({ label: badge.text, icon: iconName, iconSize: 'sm', href: linkTo, className: linkClassName })
26
+ +badgeTag( badge, { iconSize: 'sm' } )
29
27
 
30
28
  +devNames( item.longname )
31
29
 
@@ -1,4 +1,5 @@
1
1
  include _badge
2
+ include _badge-tag.pug
2
3
  include _type
3
4
  include _params
4
5
  include _return
@@ -52,10 +53,7 @@ mixin method( met )
52
53
  if isNonEmptyArray( met.badges )
53
54
  +tagWrapper()
54
55
  each badge in met.badges
55
- - const iconName = badge.text == 'private' ? 'lock' : badge.text == 'observable' ? 'eye' : null;
56
- - const linkTo = badge && badge.linkTo ? badge.linkTo : null;
57
- - const linkClassName = linkTo ? 'tag--link' : '';
58
- +tag({ label: badge.text, icon: iconName, iconSize: 'sm', href: linkTo, className: linkClassName })
56
+ +badgeTag( badge, { iconSize: 'sm' } )
59
57
 
60
58
  +devNames( met.longname, met.label )
61
59
 
@@ -1,6 +1,7 @@
1
1
  include _params
2
2
  include ./_type.pug
3
3
  include ./_badge.pug
4
+ include ./_badge-tag.pug
4
5
  include ./_dev-names.pug
5
6
  include ./_api-see-source.pug
6
7
  include ./_related.pug
@@ -26,10 +27,7 @@ mixin property( prop )
26
27
  if isNonEmptyArray( prop.badges )
27
28
  +tagWrapper()
28
29
  each badge in prop.badges
29
- - const iconName = badge.text == 'private' ? 'lock' : badge.text == 'observable' ? 'eye' : null;
30
- - const linkTo = badge && badge.linkTo ? badge.linkTo : null;
31
- - const linkClassName = linkTo ? 'tag--link' : '';
32
- +tag({ label: badge.text, icon: iconName, iconSize: 'sm', href: linkTo, className: linkClassName })
30
+ +badgeTag( badge, { iconSize: 'sm' } )
33
31
 
34
32
  +devNames( prop.longname )
35
33
 
@@ -54,4 +52,3 @@ mixin property( prop )
54
52
 
55
53
  if isNonEmptyArray( prop.see )
56
54
  +related( prop.see )
57
-
@@ -38,3 +38,7 @@ div( class="notice notice__filter api-props-filter hidden-loading" )
38
38
  input( type="checkbox" id="f5" value="deprecated" )
39
39
  label( for="f5" )
40
40
  span( class="badge badge--red" ) Deprecated
41
+ li
42
+ input( type="checkbox" id="f8" value="experimental" checked )
43
+ label( for="f8" )
44
+ span( class="badge badge--cyan" ) Experimental
@@ -2,4 +2,11 @@
2
2
  .api-subheader {
3
3
  width: 100%;
4
4
  margin-top: var(--spacing-2);
5
+
6
+ &__badges {
7
+ display: flex;
8
+ align-items: center;
9
+ flex-wrap: wrap;
10
+ gap: var(--spacing-2);
11
+ }
5
12
  }
@@ -1,10 +1,14 @@
1
1
  include ../_mixin/_badge
2
+ include ../_mixin/_badge-tag.pug
2
3
 
3
4
  mixin apiSubheader()
4
5
  - const iconName = "api-" + data.kind.toLowerCase();
5
6
  - const iconClassName = "api-icon api-icon--" + data.kind.toLowerCase();
6
7
  - const tagClassName = "tag--api tag--api-" + data.kind.toLowerCase();
8
+ - const experimentalBadge = isNonEmptyArray( data.badges ) ? data.badges.find( badge => badge.text == 'experimental' ) : null;
7
9
 
8
10
  div(class="api-subheader hidden-loading")
9
11
  div.api-subheader__badges
10
12
  +tag({ label: data.kind, icon : iconName, className: tagClassName })
13
+ if experimentalBadge
14
+ +badgeTag( experimentalBadge )
@@ -50,20 +50,23 @@ mixin dropdown({ id: dropdownId, className, preferredDirection, contentId, updat
50
50
  //- +dropdown-item({ label: 'Active item', active: true })
51
51
  //- +dropdown-item({ label: 'Item with value', value: 'custom-value' })
52
52
  //- +dropdown-item({ label: 'Item with icon', icon: 'github' })
53
- //- +dropdown-item({ label: 'Custom link', href: '/custom-link' })
53
+ //- +dropdown-item({ label: 'Custom link', href: '/custom-link', rel: 'nofollow' })
54
54
  //- +dropdown-item({ label: 'With ID', itemId: 'special-item' })
55
- mixin dropdown-item({ itemId, href, active = false, narrow, label, icon, value } = {})
55
+ mixin dropdown-item({ itemId, href, rel, active = false, narrow, label, icon, value, projectSlug, projectChannel } = {})
56
56
  - const tag = href ? 'a' : 'div';
57
57
 
58
58
  li.c-dropdown__list-item
59
59
  #{tag}.b-reset-link.c-dropdown__item(
60
60
  role='menuitem',
61
61
  href=href,
62
+ rel=rel,
62
63
  class={
63
64
  'is-active': active,
64
65
  'c-dropdown__item--narrow': !!narrow
65
66
  },
66
67
  data-item-id=itemId,
68
+ data-project-slug=projectSlug,
69
+ data-project-channel=projectChannel,
67
70
  data-value=value,
68
71
  tabindex='-1'
69
72
  )
@@ -20,6 +20,18 @@
20
20
  }
21
21
  }
22
22
 
23
+ &--experimental {
24
+ color: var(--color-cyan-600);
25
+ border-color: var(--color-cyan-600);
26
+ background-color: var(--color-cyan-50);
27
+
28
+ &.tag--link {
29
+ &:hover {
30
+ background-color: var(--color-cyan-100);
31
+ }
32
+ }
33
+ }
34
+
23
35
  &__icon {
24
36
 
25
37
  + .tag__text {
@@ -1,7 +1,8 @@
1
1
  mixin nav-project-select-dropdown()
2
2
  -
3
+ const getProjectHref = project => relative_url( page.path, `${ project.slug }/${ project.channel || 'latest' }/index.html` );
3
4
  const currentProject = projectLocals && quickNavigationProjects && quickNavigationProjects.find(
4
- project => project.slug === projectLocals.projectSlug
5
+ project => project.slug === projectLocals.projectSlug && !project.channel
5
6
  );
6
7
 
7
8
  +menu-dropdown({
@@ -10,7 +11,49 @@ mixin nav-project-select-dropdown()
10
11
  each project in quickNavigationProjects
11
12
  +dropdown-item({
12
13
  label: project.name,
13
- active: !!currentProject && project.slug === currentProject.slug,
14
- href: relative_url(page.path, `${ project.slug }/latest/index.html`)
14
+ active: !!currentProject &&
15
+ project.slug === currentProject.slug &&
16
+ ( project.channel || 'latest' ) === 'latest',
17
+ href: getProjectHref( project ),
18
+ rel: project.rel,
19
+ projectSlug: project.slug,
20
+ projectChannel: project.channel || 'latest'
15
21
  })
16
22
 
23
+ if currentProject
24
+ script.
25
+ (function() {
26
+ const script = document.currentScript;
27
+
28
+ document.addEventListener( 'DOMContentLoaded', () => {
29
+ const dropdown = script?.previousElementSibling;
30
+ const currentProjectSlug = !{ JSON.stringify( currentProject.slug ) };
31
+ const segments = window.location.pathname.split( '/' ).filter( Boolean );
32
+ const slugIndex = segments.indexOf( currentProjectSlug );
33
+
34
+ if ( !dropdown || slugIndex < 0 ) {
35
+ return;
36
+ }
37
+
38
+ const rawChannel = segments[ slugIndex + 1 ];
39
+ const currentChannel = /^\d+\.\d+\.\d+$/.test( rawChannel || '' ) ? 'latest' : ( rawChannel || 'latest' );
40
+ const items = Array.from( dropdown.querySelectorAll( '.c-dropdown__item[data-project-slug][data-project-channel]' ) );
41
+ const activeItem = items.find( item => item.dataset.projectSlug === currentProjectSlug && item.dataset.projectChannel === currentChannel );
42
+
43
+ if ( !activeItem ) {
44
+ return;
45
+ }
46
+
47
+ for ( const item of items ) {
48
+ item.classList.toggle( 'is-active', item === activeItem );
49
+ }
50
+
51
+ const labelElement = dropdown.querySelector( '.c-menu-dropdown__button .c-button__label' );
52
+ const labelText = activeItem.querySelector( 'span' )?.textContent?.trim() || activeItem.textContent.trim();
53
+
54
+ if ( labelElement && labelText ) {
55
+ labelElement.textContent = labelText;
56
+ }
57
+ }, { once: true } );
58
+ })();
59
+
@@ -3,6 +3,7 @@ include ./slide-parts
3
3
  mixin mobile-nav()
4
4
  -
5
5
  const currentProject = projectLocals && projectsData.find( project => project.slug === projectLocals.projectSlug );
6
+ const getProjectHref = project => relative_url( page.path, `${ project.slug }/${ project.channel || 'latest' }/index.html` );
6
7
  let initialSlideId = projectLocals ? `project-${projectLocals.projectSlug}` : 'products';
7
8
 
8
9
  // If the page has a category, set the initial slide to that category's slide
@@ -23,20 +24,54 @@ mixin mobile-nav()
23
24
  if quickNavigationProjects
24
25
  ol.b-reset-list.c-mobile-nav__items
25
26
  each project in quickNavigationProjects
26
- - isCurrentProject = projectLocals && project.slug === projectLocals.projectSlug
27
-
27
+ - isCurrentProject = projectLocals && project.slug === projectLocals.projectSlug && !project.channel
28
28
  if isCurrentProject
29
29
  +mobile-nav-item({
30
30
  label: project.name,
31
31
  active: true,
32
- targetSlideId: `project-${ project.slug }`
32
+ targetSlideId: `project-${ project.slug }`,
33
+ projectSlug: project.slug,
34
+ projectChannel: project.channel || 'latest'
33
35
  })
34
36
  else
35
37
  +mobile-nav-item({
36
- href: relative_url(page.path, `${project.slug}/latest/index.html`),
37
- label: project.name
38
+ href: getProjectHref( project ),
39
+ label: project.name,
40
+ rel: project.rel,
41
+ projectSlug: project.slug,
42
+ projectChannel: project.channel || 'latest'
38
43
  })
39
44
 
45
+ if currentProject
46
+ script.
47
+ (function() {
48
+ const script = document.currentScript;
49
+
50
+ document.addEventListener( 'DOMContentLoaded', () => {
51
+ const mobileNav = script?.previousElementSibling;
52
+ const currentProjectSlug = !{ JSON.stringify( currentProject.slug ) };
53
+ const segments = window.location.pathname.split( '/' ).filter( Boolean );
54
+ const slugIndex = segments.indexOf( currentProjectSlug );
55
+
56
+ if ( !mobileNav || slugIndex < 0 ) {
57
+ return;
58
+ }
59
+
60
+ const rawChannel = segments[ slugIndex + 1 ];
61
+ const currentChannel = /^\d+\.\d+\.\d+$/.test( rawChannel || '' ) ? 'latest' : ( rawChannel || 'latest' );
62
+ const items = Array.from( mobileNav.querySelectorAll( '.c-mobile-nav__item[data-project-slug][data-project-channel]' ) );
63
+ const activeItem = items.find( item => item.dataset.projectSlug === currentProjectSlug && item.dataset.projectChannel === currentChannel );
64
+
65
+ if ( !activeItem ) {
66
+ return;
67
+ }
68
+
69
+ for ( const item of items ) {
70
+ item.classList.toggle( 'is-active', item === activeItem );
71
+ }
72
+ }, { once: true } );
73
+ })();
74
+
40
75
  //- Current project slide
41
76
  if projectLocals
42
77
  +mobile-nav-slide({ slideId: `project-${ projectLocals.projectSlug }` })
@@ -32,7 +32,7 @@ mixin mobile-nav-heading({ href, label })
32
32
  iconPosition: 'right',
33
33
  })
34
34
 
35
- mixin mobile-nav-item({ href, label, targetSlideId, target, active = false })
35
+ mixin mobile-nav-item({ href, label, rel, targetSlideId, target, active = false, projectSlug, projectChannel })
36
36
  li
37
37
  if targetSlideId
38
38
  +button({
@@ -48,11 +48,16 @@ mixin mobile-nav-item({ href, label, targetSlideId, target, active = false })
48
48
  'data-navigate-direction'='forward',
49
49
  'aria-controls'=`slide-${targetSlideId}`,
50
50
  'aria-expanded'='false',
51
- 'aria-haspopup'='true'
51
+ 'aria-haspopup'='true',
52
+ 'data-project-slug'=projectSlug,
53
+ 'data-project-channel'=projectChannel
52
54
  )
53
55
  else
54
56
  a.b-link.c-mobile-nav__item(
55
57
  href=href,
58
+ rel=rel,
59
+ data-project-slug=projectSlug,
60
+ data-project-channel=projectChannel,
56
61
  class=active ? 'is-active' : '',
57
62
  target=target
58
63
  )= label
@@ -191,6 +191,8 @@ div
191
191
  +checkbox({ id:'f5', name: 'name7', label: 'Deprecated', value: 'deprecated', expanded: true })
192
192
  +dropdown-item()
193
193
  +checkbox({ id:'f7', name: 'name4', label: 'Internal', value: 'internal', expanded: true })
194
+ +dropdown-item()
195
+ +checkbox({ id:'f8', name: 'name8', label: 'Experimental', value: 'experimental', expanded: true, checked: true })
194
196
  +dropdown-item()
195
197
  +checkbox({ id:'f6', name: 'name3', label: 'Mixed', value: 'mixed', expanded: true })
196
198
 
@@ -38,3 +38,7 @@ div( class="notice notice__filter api-props-filter hidden-loading" )
38
38
  input( type="checkbox" id="f5" value="deprecated" )
39
39
  label( for="f5" )
40
40
  span( class="badge badge--red" ) Deprecated
41
+ li
42
+ input( type="checkbox" id="f8" value="experimental" checked )
43
+ label( for="f8" )
44
+ span( class="badge badge--cyan" ) Experimental
@@ -139,21 +139,23 @@ export class ApiFilter extends BaseComponent {
139
139
  * Sets the initial state of the API filter into local storage when there is no set yet.
140
140
  */
141
141
  setInitialApiFilterState() {
142
- const { apiFilterState } = this.state;
143
142
  const { filterDropdown } = this.elements;
143
+ const apiFilterState = { ...this.state.apiFilterState };
144
144
 
145
- if ( Object.keys( apiFilterState ).length === 0 ) {
146
- Array.from( filterDropdown.querySelectorAll( '.js-api-filter-checkbox input' ) ).forEach( input => {
147
- const isChecked = input.checked;
148
- const filterType = input.value;
145
+ Array.from( filterDropdown.querySelectorAll( '.js-api-filter-checkbox input' ) ).forEach( input => {
146
+ const isChecked = input.checked;
147
+ const filterType = input.value;
149
148
 
150
- this.saveApiFilterState( filterType, isChecked );
151
- } );
152
- }
149
+ if ( apiFilterState[ filterType ] === undefined ) {
150
+ apiFilterState[ filterType ] = isChecked;
151
+ }
152
+ } );
153
+
154
+ window.localStorage.setItem( API_FILTER_LOCAL_STORAGE_KEY, JSON.stringify( apiFilterState ) );
153
155
 
154
156
  this.setState( {
155
- apiFilterState: JSON.parse( localStorage.getItem( API_FILTER_LOCAL_STORAGE_KEY ) ) || {},
156
- activeFilters: Object.keys( this.state.apiFilterState ).filter( key => this.state.apiFilterState[ key ] )
157
+ apiFilterState,
158
+ activeFilters: Object.keys( apiFilterState ).filter( key => apiFilterState[ key ] )
157
159
  } );
158
160
  }
159
161
 
@@ -163,14 +165,21 @@ export class ApiFilter extends BaseComponent {
163
165
  initialApplyApiFilterState() {
164
166
  const { apiFilterState } = this.state;
165
167
  const { filterDropdown } = this.elements;
168
+ const activeFilters = [];
166
169
 
167
170
  Array.from( filterDropdown.querySelectorAll( '.js-api-filter-checkbox input' ) ).forEach( input => {
168
171
  const isChecked = input.checked;
169
172
  const filterType = input.value;
170
173
 
171
174
  input.checked = apiFilterState[ filterType ] !== undefined ? apiFilterState[ filterType ] : isChecked;
175
+
176
+ if ( input.checked ) {
177
+ activeFilters.push( filterType );
178
+ }
172
179
  } );
173
180
 
181
+ this.setState( { activeFilters } );
182
+
174
183
  // this.dispatchInitialFolderStateDoneEvent();
175
184
  }
176
185
 
@@ -51,6 +51,7 @@
51
51
  @include badge( 'red' );
52
52
  @include badge( 'orange' );
53
53
  @include badge( 'purple' );
54
+ @include badge( 'cyan' );
54
55
 
55
56
  /* #357. */
56
57
  &.badge--observable {
@@ -32,6 +32,7 @@ $u-colors: (
32
32
  'badge-red': #E50037,
33
33
  'badge-black': #212121,
34
34
  'badge-purple': #AB47BC,
35
+ 'badge-cyan': hsl(189, 100%, 31%),
35
36
 
36
37
  // Hints
37
38
  'hint-info': #64B5F6,
@@ -14,6 +14,7 @@ export function enableFiltering() {
14
14
  protected: storagePrefix + 'protected',
15
15
  private: storagePrefix + 'private',
16
16
  deprecated: storagePrefix + 'deprecated',
17
+ experimental: storagePrefix + 'experimental',
17
18
  tree: 'filtering:tree'
18
19
  };
19
20