-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Add an "ide" component #13708
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ericwindmill
wants to merge
35
commits into
main
Choose a base branch
from
ide-component
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Add an "ide" component #13708
Changes from all commits
Commits
Show all changes
35 commits
Select commit
Hold shift + click to select a range
59af3f3
initial checkin
ericwindmill 675e7ac
Merge branch 'main' of https://github.com/flutter/website into ide-co…
ericwindmill 78a396d
fix filename
ericwindmill 66d2e40
checkin some refactoring
ericwindmill ae00cb1
rework data format
ericwindmill 485f2ab
start refactor
ericwindmill 022ccaf
start refactor
ericwindmill abac75d
more refactor
ericwindmill 9fce03b
more refactor
ericwindmill d8b7734
more refactor
ericwindmill d13168d
more refactor
ericwindmill 1862e74
more refactor
ericwindmill 1373068
more refactor
ericwindmill f083b82
tidy scss
ericwindmill e944483
fix scss funkiness
ericwindmill 95d3c93
more refactor
ericwindmill 6eb79db
more refactor
ericwindmill 2ff76c9
more refactor
ericwindmill 68a0f0c
Merge branch 'main' of https://github.com/flutter/website into ide-co…
ericwindmill 63e6fdc
Merge branch 'main' of https://github.com/flutter/website into ide-co…
ericwindmill e7c3351
address code review
ericwindmill 433d743
Merge branch 'main' of https://github.com/flutter/website into ide-co…
ericwindmill 3d280b1
Merge branch 'main' of https://github.com/flutter/website into ide-co…
ericwindmill a1d8866
oops
ericwindmill cc7535e
Merge branch 'main' into ide-component
ericwindmill 359bbc0
Merge branch 'main' into ide-component
ericwindmill bf6e0bd
Merge branch 'main' of https://github.com/flutter/website into ide-co…
ericwindmill 9a4decf
Merge branch 'main' of https://github.com/flutter/website into ide-co…
ericwindmill a2ae9e2
Merge branch 'main' into ide-component
ericwindmill 6bde3fe
Merge branch 'main' of https://github.com/flutter/website into ide-co…
ericwindmill 147c225
address some PR comments, break up large file
ericwindmill 873b9dc
remove unused scss var
ericwindmill f8bfaa9
make scss vars sane
ericwindmill 270349b
Merge branch 'main' of https://github.com/flutter/website into ide-co…
ericwindmill acd7a64
Merge branch 'main' of https://github.com/flutter/website into ide-co…
ericwindmill File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
524 changes: 524 additions & 0 deletions
524
packages/site_shared/lib/_sass/components/_ide-explorer.scss
Large diffs are not rendered by default.
Oops, something went wrong.
702 changes: 702 additions & 0 deletions
702
packages/site_shared/lib/components/common/ide_explorer/ide_explorer.dart
Large diffs are not rendered by default.
Oops, something went wrong.
254 changes: 254 additions & 0 deletions
254
packages/site_shared/lib/components/common/ide_explorer/markdown_component.dart
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,254 @@ | ||
| import 'package:jaspr/jaspr.dart'; | ||
| import 'package:jaspr_content/jaspr_content.dart'; | ||
|
|
||
| import '../../../util.dart'; | ||
| import 'ide_explorer.dart'; | ||
| import 'models.dart'; | ||
|
|
||
| /// A custom markdown component that parses `<IdeExplorer>` and its | ||
| /// `<IdeProjectRoot>`, `<IdeFolder>`, and `<IdePage>` children. Defers | ||
| /// building the IDE html to the [IdeExplorer] component. | ||
| class IdeExplorerMarkdownComponent extends CustomComponent { | ||
| const IdeExplorerMarkdownComponent() : super.base(); | ||
|
|
||
| // Tag name constants | ||
| static const String _tagIdeExplorer = 'IdeExplorer'; | ||
| static const String _tagIdeProjectRoot = 'IdeProjectRoot'; | ||
| static const String _tagIdeFolder = 'IdeFolder'; | ||
| static const String _tagIdePage = 'IdePage'; | ||
|
|
||
| // Attribute name constants | ||
| static const String _attrId = 'id'; | ||
| static const String _attrLabel = 'label'; | ||
| static const String _attrIsDefaultPage = 'is-default-page'; | ||
| static const String _attrStartsClosed = 'starts-closed'; | ||
| static const String _attrBadge = 'badge'; | ||
| static const String _attrBadgeColor = 'badge-color'; | ||
| static const String _attrSubtitle = 'subtitle'; | ||
|
|
||
| // Default values | ||
| static const String _defaultRootPrefix = 'root'; | ||
| static const String _defaultNodePrefix = 'node'; | ||
|
|
||
| @override | ||
| Component? create(Node node, NodesBuilder builder) { | ||
| if (node is! ElementNode || node.tag != _tagIdeExplorer) { | ||
| return null; | ||
| } | ||
|
|
||
| final projectRootElements = node.children | ||
| ?.whereType<ElementNode>() | ||
| .where((n) => n.tag == _tagIdeProjectRoot) | ||
| .toList(); | ||
|
|
||
| if (projectRootElements == null || projectRootElements.isEmpty) { | ||
| print( | ||
| '[ERROR] <$_tagIdeExplorer> requires at ' | ||
| 'least one <$_tagIdeProjectRoot> child element.', | ||
| ); | ||
| return const Component.empty(); | ||
| } | ||
|
|
||
| final customContents = <String, Component>{}; | ||
| final roots = <IdeExplorerProjectRoot>[]; | ||
| for (final (index, rootEl) in projectRootElements.indexed) { | ||
| final rootId = _generateNodeId( | ||
| rootEl.attributes, | ||
| _defaultRootPrefix, | ||
| index, | ||
| ); | ||
| roots.add( | ||
| IdeExplorerProjectRoot( | ||
| id: rootId, | ||
| label: rootEl.attributes[_attrLabel], | ||
| children: _parseTreeNodes( | ||
| rootEl.children, | ||
| builder, | ||
| customContents, | ||
| rootId, | ||
| ), | ||
| ), | ||
| ); | ||
| } | ||
|
|
||
| return IdeExplorer( | ||
| roots: roots, | ||
| customContents: customContents, | ||
| ); | ||
| } | ||
|
|
||
| /// Generates a node ID from attributes or creates a default one. | ||
| String _generateNodeId( | ||
| Map<String, String> attributes, | ||
| String prefix, | ||
| int index, [ | ||
| String? parentId, | ||
| ]) { | ||
| if (attributes[_attrId] != null) { | ||
| return attributes[_attrId]!; | ||
| } | ||
| final label = attributes[_attrLabel]; | ||
| final localId = (label != null && label.isNotEmpty) | ||
| ? slugify(label) | ||
| : '$prefix-$index'; | ||
| return parentId != null ? '$parentId-$localId' : localId; | ||
| } | ||
|
ericwindmill marked this conversation as resolved.
|
||
|
|
||
| List<IdeTreeNode> _parseTreeNodes( | ||
| List<Node>? nodes, | ||
| NodesBuilder builder, | ||
| Map<String, Component> customContents, | ||
| String parentId, | ||
| ) { | ||
| if (nodes == null || nodes.isEmpty) return const []; | ||
|
|
||
| final result = <IdeTreeNode>[]; | ||
|
|
||
| for (final (index, child) in nodes.whereType<ElementNode>().indexed) { | ||
| if (child.tag != _tagIdeFolder && child.tag != _tagIdePage) { | ||
| continue; | ||
| } | ||
|
|
||
| final treeNode = _buildTreeNodeFromElement( | ||
| child, | ||
| index, | ||
| builder, | ||
| customContents, | ||
| parentId, | ||
| ); | ||
|
|
||
| result.add(treeNode); | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
|
ericwindmill marked this conversation as resolved.
|
||
|
|
||
| /// Builds a single [IdeTreeNode] from an [ElementNode]. | ||
| IdeTreeNode _buildTreeNodeFromElement( | ||
| ElementNode element, | ||
| int index, | ||
| NodesBuilder builder, | ||
| Map<String, Component> customContents, | ||
| String parentId, | ||
| ) { | ||
| final attributes = element.attributes; | ||
| final label = attributes[_attrLabel] ?? ''; | ||
| final id = _generateNodeId(attributes, _defaultNodePrefix, index, parentId); | ||
|
|
||
| // Parse boolean attributes | ||
| final isDefaultPage = _getBoolAttribute( | ||
| attributes, | ||
| _attrIsDefaultPage, | ||
| defaultValue: false, | ||
| ); | ||
|
|
||
| // Parse badge attributes | ||
| final badge = attributes[_attrBadge]; | ||
| final badgeColor = attributes[_attrBadgeColor] != null | ||
| ? IdeBadgeColor.fromString(attributes[_attrBadgeColor]) | ||
| : null; | ||
|
|
||
| final subtitle = attributes[_attrSubtitle]; | ||
|
|
||
| // Extract and store custom body content if present | ||
| _storeCustomContentIfPresent( | ||
| element.children, | ||
| id, | ||
| builder, | ||
| customContents, | ||
| ); | ||
|
|
||
| if (element.tag == _tagIdeFolder) { | ||
| final startsClosed = _getBoolAttribute( | ||
| attributes, | ||
| _attrStartsClosed, | ||
| defaultValue: true, | ||
| ); | ||
|
|
||
| // Recursively parse children | ||
| final nestedTreeNodes = _parseTreeNodes( | ||
| element.children, | ||
| builder, | ||
| customContents, | ||
| id, | ||
| ); | ||
|
|
||
| return IdeFolderNode( | ||
| id: id, | ||
| label: label, | ||
| isDefaultPage: isDefaultPage, | ||
| startsClosed: startsClosed, | ||
| badge: badge, | ||
| badgeColor: badgeColor, | ||
| subtitle: subtitle, | ||
| children: nestedTreeNodes, | ||
| ); | ||
| } | ||
|
|
||
| return IdeFileNode( | ||
| id: id, | ||
| label: label, | ||
| isDefaultPage: isDefaultPage, | ||
| badge: badge, | ||
| badgeColor: badgeColor, | ||
| subtitle: subtitle, | ||
| ); | ||
| } | ||
|
|
||
| /// Parses a boolean attribute value, returning [defaultValue] if not present. | ||
| bool _getBoolAttribute( | ||
| Map<String, String> attributes, | ||
| String key, { | ||
| required bool defaultValue, | ||
| }) { | ||
| final value = attributes[key]; | ||
| return value != null ? value == 'true' : defaultValue; | ||
| } | ||
|
|
||
| /// Checks if a node represents body content (not a folder/page structure). | ||
| bool _isBodyContent(Node node) { | ||
| if (node is ElementNode && | ||
| (node.tag == _tagIdeFolder || node.tag == _tagIdePage)) { | ||
| return false; | ||
| } | ||
| if (node is TextNode && node.text.trim().isEmpty) { | ||
| return false; | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| /// Extracts non-structural content nodes from children. | ||
| List<Node> _extractContentNodes(List<Node> children) { | ||
| return children | ||
| .where((n) { | ||
| if (n is ElementNode && | ||
| (n.tag == _tagIdeFolder || n.tag == _tagIdePage)) { | ||
| return false; | ||
| } | ||
| return true; | ||
| }) | ||
| .toList(growable: false); | ||
| } | ||
|
|
||
| /// Stores custom body content for a node if it exists. | ||
| void _storeCustomContentIfPresent( | ||
| List<Node>? children, | ||
| String nodeId, | ||
| NodesBuilder builder, | ||
| Map<String, Component> customContents, | ||
| ) { | ||
| if (children == null || !_hasCustomBodyContent(children)) { | ||
| return; | ||
| } | ||
|
|
||
| final contentNodes = _extractContentNodes(children); | ||
| if (contentNodes.isNotEmpty) { | ||
| customContents[nodeId] = builder.build(contentNodes); | ||
| } | ||
| } | ||
|
|
||
| /// Checks if a list of nodes has custom body content. | ||
| bool _hasCustomBodyContent(List<Node> children) { | ||
| return children.any(_isBodyContent); | ||
| } | ||
| } | ||
76 changes: 76 additions & 0 deletions
76
packages/site_shared/lib/components/common/ide_explorer/models.dart
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| // Copyright 2026 The Flutter Authors. All rights reserved. | ||
| // Use of this source code is governed by a BSD-style license that can be | ||
| // found in the LICENSE file. | ||
|
|
||
| /// A single top-level file tree shown by an [IdeExplorer], which may | ||
| /// have multiple roots to represent multiple packages | ||
| /// (for example, "Project" and "Shared"). | ||
| /// | ||
| /// When multiple roots are provided, they are rendered as tabs that can be | ||
| /// switched between. | ||
| class const IdeExplorerProjectRoot({ | ||
| required final String id, | ||
| final String? label, | ||
| final List<IdeTreeNode> children = const [], | ||
| }); | ||
|
|
||
| /// A single entry in an [IdeExplorer] tree. | ||
| /// | ||
| /// Subclasses: | ||
| /// - [IdeFileNode] for file / document entries. | ||
| /// - [IdeFolderNode] for directory entries that can contain nested children. | ||
| sealed class const IdeTreeNode({ | ||
| required final String id, | ||
| required final String label, | ||
| final bool isDefaultPage = false, | ||
| final String? badge, | ||
| final IdeBadgeColor? badgeColor, | ||
| final String? title, | ||
| final String? subtitle, | ||
| }); | ||
|
|
||
| /// A file entry in an [IdeExplorer] tree. | ||
| class const IdeFileNode({ | ||
| required super.id, | ||
| required super.label, | ||
| super.isDefaultPage, | ||
| super.badge, | ||
| super.badgeColor, | ||
| super.title, | ||
| super.subtitle, | ||
| }) extends IdeTreeNode; | ||
|
|
||
| /// A directory entry in an [IdeExplorer] tree that can contain nested children. | ||
| class const IdeFolderNode({ | ||
| required super.id, | ||
| required super.label, | ||
| super.isDefaultPage, | ||
| final bool startsClosed = true, | ||
| final List<IdeTreeNode> children = const [], | ||
| super.badge, | ||
| super.badgeColor, | ||
| super.title, | ||
| super.subtitle, | ||
| }) extends IdeTreeNode; | ||
|
|
||
| /// Corresponds to colors used for badges on an [IdeTreeNode]. | ||
| enum IdeBadgeColor { | ||
| neutral, | ||
| info, | ||
| tip, | ||
| important, | ||
| warning, | ||
| error; | ||
|
|
||
| static IdeBadgeColor fromString(String? data) { | ||
| if (data == null) return IdeBadgeColor.neutral; | ||
| return switch (data.toLowerCase()) { | ||
| 'info' => IdeBadgeColor.info, | ||
| 'tip' => IdeBadgeColor.tip, | ||
| 'important' => IdeBadgeColor.important, | ||
| 'warning' => IdeBadgeColor.warning, | ||
| 'error' => IdeBadgeColor.error, | ||
| _ => IdeBadgeColor.neutral, | ||
| }; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.