From e21c2473b333561de45c31f6b643e9a9934b5274 Mon Sep 17 00:00:00 2001 From: Alan Treadway Date: Mon, 19 Jan 2026 09:18:17 +0000 Subject: [PATCH 01/12] git subrepo pull external/ag-shared subrepo: subdir: "external/ag-shared" merged: "2c81ab2253a" upstream: origin: "https://github.com/ag-grid/ag-shared.git" branch: "latest" commit: "2c81ab2253a" git-subrepo: version: "0.4.9" origin: "https://github.com/Homebrew/brew" commit: "70badad488" --- external/ag-shared/.gitrepo | 4 +- .../prompts/patches/rulesync+5.2.0.patch | 71 +++ external/ag-shared/scripts/apply-patches.sh | 51 ++ .../scripts/setup-prompts/setup-prompts.sh | 198 ++++++- .../scripts/shard/calculate-pkg-shards.js | 2 + .../scripts/sync-rulesync/sync-rulesync.sh | 509 ++++++++++++++++++ 6 files changed, 811 insertions(+), 24 deletions(-) create mode 100644 external/ag-shared/prompts/patches/rulesync+5.2.0.patch create mode 100755 external/ag-shared/scripts/apply-patches.sh create mode 100755 external/ag-shared/scripts/sync-rulesync/sync-rulesync.sh diff --git a/external/ag-shared/.gitrepo b/external/ag-shared/.gitrepo index 92f233ae4d5..7e0980f9988 100644 --- a/external/ag-shared/.gitrepo +++ b/external/ag-shared/.gitrepo @@ -6,7 +6,7 @@ [subrepo] remote = https://github.com/ag-grid/ag-shared.git branch = latest - commit = 7e82fede2e46b228b3c52cfacab0df9b2cb02280 - parent = d16b9c9641036859f0ee644391ae24dec213125e + commit = 2c81ab2253a675b7e071ffc37b822d08f1329b51 + parent = 4e6337f2694f5d855e4570d697dcc944b6c0d149 method = rebase cmdver = 0.4.9 diff --git a/external/ag-shared/prompts/patches/rulesync+5.2.0.patch b/external/ag-shared/prompts/patches/rulesync+5.2.0.patch new file mode 100644 index 00000000000..54f3edff437 --- /dev/null +++ b/external/ag-shared/prompts/patches/rulesync+5.2.0.patch @@ -0,0 +1,71 @@ +diff --git a/node_modules/rulesync/dist/index.js b/node_modules/rulesync/dist/index.js +index 23dce30..0d9de20 100755 +--- a/node_modules/rulesync/dist/index.js ++++ b/node_modules/rulesync/dist/index.js +@@ -86,7 +86,7 @@ import { resolve as resolve2 } from "path"; + import { parse as parseJsonc } from "jsonc-parser"; + + // src/utils/file.ts +-import { globSync } from "fs"; ++import { globSync, statSync } from "fs"; + import { mkdir, readdir, readFile, rm, stat, writeFile } from "fs/promises"; + import os from "os"; + import { dirname, join, relative, resolve } from "path"; +@@ -171,13 +171,28 @@ async function listDirectoryFiles(dir) { + async function findFilesByGlobs(globs, options = {}) { + const { type = "all" } = options; + const items = globSync(globs, { withFileTypes: true }); ++ // Filter out broken symlinks - they exist as symlinks but their target doesn't exist ++ const validItems = items.filter((item) => { ++ if (item.isSymbolicLink()) { ++ try { ++ statSync(join(item.parentPath, item.name)); ++ return true; ++ } catch (err) { ++ if (err.code === "ENOENT") { ++ return false; ++ } ++ throw err; ++ } ++ } ++ return true; ++ }); + switch (type) { + case "file": +- return items.filter((item) => item.isFile()).map((item) => join(item.parentPath, item.name)); ++ return validItems.filter((item) => item.isFile()).map((item) => join(item.parentPath, item.name)); + case "dir": +- return items.filter((item) => item.isDirectory()).map((item) => join(item.parentPath, item.name)); ++ return validItems.filter((item) => item.isDirectory()).map((item) => join(item.parentPath, item.name)); + case "all": +- return items.map((item) => join(item.parentPath, item.name)); ++ return validItems.map((item) => join(item.parentPath, item.name)); + default: + throw new Error(`Invalid type: ${type}`); + } +@@ -10595,12 +10610,7 @@ var toolRuleFactories = /* @__PURE__ */ new Map([ + meta: { + extension: "md", + supportsGlobal: false, +- ruleDiscoveryMode: "toon", +- additionalConventions: { +- commands: { commandClass: AgentsmdCommand }, +- subagents: { subagentClass: AgentsmdSubagent }, +- skills: { skillClass: AgentsmdSkill } +- } ++ ruleDiscoveryMode: "auto" + } + } + ], +@@ -10653,10 +10663,7 @@ var toolRuleFactories = /* @__PURE__ */ new Map([ + meta: { + extension: "md", + supportsGlobal: true, +- ruleDiscoveryMode: "toon", +- additionalConventions: { +- subagents: { subagentClass: CodexCliSubagent } +- } ++ ruleDiscoveryMode: "auto" + } + } + ], diff --git a/external/ag-shared/scripts/apply-patches.sh b/external/ag-shared/scripts/apply-patches.sh new file mode 100755 index 00000000000..bd09f9247a8 --- /dev/null +++ b/external/ag-shared/scripts/apply-patches.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# Apply patches with recovery for stale cached packages. +# +# When CI restores a cached node_modules that contains already-patched or +# differently-patched packages, patch-package fails. This script detects +# that scenario and recovers by removing the affected packages and +# reinstalling them before retrying. + +set -e + +# First attempt - try applying patches normally +if npx patch-package; then + exit 0 +fi + +echo "" +echo "Patch failed, attempting recovery..." +echo "" + +# Remove all patched packages so they can be reinstalled fresh +for patch_file in patches/*.patch; do + [[ -f "$patch_file" ]] || continue + filename=$(basename "$patch_file") + + # Extract package name from patch filename + # Handles: @scope+pkg+version.patch -> @scope/pkg + # pkg+version.patch -> pkg + if [[ "$filename" =~ ^@([^+]+)\+([^+]+)\+.+\.patch$ ]]; then + pkg="@${BASH_REMATCH[1]}/${BASH_REMATCH[2]}" + elif [[ "$filename" =~ ^([^+]+)\+.+\.patch$ ]]; then + pkg="${BASH_REMATCH[1]}" + else + continue + fi + + if [[ -d "node_modules/$pkg" ]]; then + echo "Removing node_modules/$pkg" + rm -rf "node_modules/$pkg" + fi +done + +# Reinstall removed packages without triggering postinstall again +# --ignore-scripts prevents infinite loop +echo "" +echo "Reinstalling packages..." +yarn install --ignore-scripts --check-files + +# Retry patches - this should now succeed +echo "" +echo "Retrying patches..." +npx patch-package diff --git a/external/ag-shared/scripts/setup-prompts/setup-prompts.sh b/external/ag-shared/scripts/setup-prompts/setup-prompts.sh index c51f48788db..18a5017e1f2 100755 --- a/external/ag-shared/scripts/setup-prompts/setup-prompts.sh +++ b/external/ag-shared/scripts/setup-prompts/setup-prompts.sh @@ -24,6 +24,63 @@ YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color +# Get the main repo root (handles worktrees) +# In a worktree, .git is a file containing "gitdir: /path/to/main/.git/worktrees/name" +get_main_repo_root() { + local git_path="$REPO_ROOT/.git" + + if [[ -f "$git_path" ]]; then + # We're in a worktree - parse the gitdir to find main repo + local gitdir + gitdir=$(cat "$git_path" | sed 's/gitdir: //') + # gitdir is like /path/to/main/.git/worktrees/name + # Go up twice to get /path/to/main/.git, then dirname for main repo + local main_git_dir + main_git_dir=$(dirname "$(dirname "$gitdir")") + dirname "$main_git_dir" + else + # Normal checkout - current directory is the repo root + echo "$REPO_ROOT" + fi +} + +# Detect if we're in a worktree +is_worktree() { + [[ -f "$REPO_ROOT/.git" ]] +} + +# Detect CI environment +is_ci() { + [[ -n "${CI:-}" || -n "${GITHUB_ACTIONS:-}" || -n "${JENKINS_URL:-}" || -n "${BUILDKITE:-}" || -n "${CIRCLECI:-}" || -n "${TRAVIS:-}" ]] +} + +# Detect if running in interactive terminal +is_interactive() { + [[ -t 0 ]] +} + +# Check if user has access to the prompts repo +has_repo_access() { + git ls-remote "$PROMPTS_REPO" HEAD >/dev/null 2>&1 +} + +# Prompt user with yes/no (returns 0 for yes, 1 for no) +prompt_yes_no() { + local message="$1" + if ! is_interactive; then + return 1 # Non-interactive: default to no + fi + read -p "$message [Y/n] " -n 1 -r + echo + [[ ! $REPLY =~ ^[Nn]$ ]] +} + +# Configuration for prompts repository +PROMPTS_REPO="${AG_PROMPTS_REPO:-git@github.com:ag-grid/ag-charts-prompts.git}" +PROMPTS_DIR_NAME="${AG_PROMPTS_DIR_NAME:-ag-charts-prompts}" +MAIN_REPO_ROOT=$(get_main_repo_root) +PROMPTS_DIR="$MAIN_REPO_ROOT/../$PROMPTS_DIR_NAME" + # Tool detection functions # Each function returns 0 if tool is detected, 1 otherwise @@ -145,6 +202,97 @@ kiro:Kiro IDE:detect_kiro # Use --targets=agentsmd explicitly if needed EXCLUDED_TOOLS="agentsmd" +# Check if prompts checkout is behind remote +is_prompts_behind() { + ( + cd "$PROMPTS_DIR" + git fetch origin --quiet 2>/dev/null || return 1 + local LOCAL=$(git rev-parse HEAD) + local REMOTE=$(git rev-parse origin/latest 2>/dev/null || git rev-parse origin/main 2>/dev/null || echo "") + [[ -n "$REMOTE" && "$LOCAL" != "$REMOTE" ]] + ) 2>/dev/null +} + +# Setup worktree symlink for prompts +# In worktrees, create a symlink in the parent directory pointing to the real prompts +# This allows the version-controlled relative symlink (external/prompts) to work +setup_worktree_prompts_symlink() { + if is_worktree; then + local worktree_parent + worktree_parent=$(dirname "$REPO_ROOT") + local parent_prompts_link="$worktree_parent/$PROMPTS_DIR_NAME" + local real_prompts + real_prompts=$(cd "$PROMPTS_DIR" && pwd) + + if [[ ! -e "$parent_prompts_link" ]]; then + ln -sf "$real_prompts" "$parent_prompts_link" || true + elif [[ -L "$parent_prompts_link" ]]; then + local current_target + current_target=$(readlink "$parent_prompts_link") + if [[ "$current_target" != "$real_prompts" ]]; then + ln -sf "$real_prompts" "$parent_prompts_link" || true + fi + fi + fi +} + +# Check and setup prompts repository +# Returns 0 if prompts are available or not needed, 1 if needed but not available +setup_prompts_repo() { + # Only run if this repo has an external/prompts symlink (e.g., ag-charts) + # Other repos (e.g., ag-grid) don't use this mechanism + if [[ ! -L "$REPO_ROOT/external/prompts" ]]; then + return 0 + fi + + # Skip in CI - prompts are optional + if is_ci; then + return 0 + fi + + # Check if prompts directory exists + if [[ -d "$PROMPTS_DIR" ]]; then + # Prompts exist - check if we should update + if is_prompts_behind; then + echo -e "${YELLOW}$PROMPTS_DIR_NAME is out of date.${NC}" + if is_interactive && prompt_yes_no "Update now?"; then + echo "Updating $PROMPTS_DIR_NAME..." + if ! (cd "$PROMPTS_DIR" && git pull --ff-only); then + echo -e "${YELLOW}Warning: Failed to update $PROMPTS_DIR_NAME, continuing with current version${NC}" + fi + fi + fi + + # Setup worktree symlink if needed + setup_worktree_prompts_symlink + return 0 + fi + + # Prompts directory doesn't exist + if is_interactive; then + # Interactive: offer to clone + if has_repo_access; then + echo -e "${YELLOW}$PROMPTS_DIR_NAME not found at $PROMPTS_DIR${NC}" + if prompt_yes_no "Clone it now?"; then + echo "Cloning $PROMPTS_DIR_NAME..." + if git clone "$PROMPTS_REPO" "$PROMPTS_DIR"; then + setup_worktree_prompts_symlink + return 0 + else + echo -e "${YELLOW}Warning: Failed to clone $PROMPTS_DIR_NAME${NC}" + fi + fi + else + echo -e "${YELLOW}No access to $PROMPTS_DIR_NAME repository${NC}" + fi + else + # Non-interactive: just warn + echo -e "${YELLOW}Warning: $PROMPTS_DIR_NAME not found - rulesync may not have completely setup tooling${NC}" + fi + + return 1 +} + # Detect all installed tools detect_tools() { local detected="" @@ -251,33 +399,36 @@ generate_config() { if [[ "$verbose" == "true" ]]; then echo -e "${BLUE}Generating configurations for: ${NC}$targets" echo "" + fi - # Run rulesync with detected targets (verbose) - npx rulesync generate \ - --targets="$targets" \ - --features="rules,ignore,mcp,commands,subagents" \ - --delete + # Run rulesync and capture output + exit code + local output + local exit_code=0 + output=$(npx rulesync generate \ + --targets="$targets" \ + --features="rules,ignore,mcp,commands,subagents" \ + --delete 2>&1) || exit_code=$? - # Copy extra configs + if [[ $exit_code -eq 0 ]]; then copy_extra_configs "$verbose" "$targets" - echo "" - echo -e "${GREEN}✓ Configuration generated successfully${NC}" + if [[ "$verbose" == "true" ]]; then + echo "$output" + echo "" + echo -e "${GREEN}✓ Configuration generated successfully${NC}" + else + local summary + summary=$(echo "$output" | grep -o '🎉.*' || echo "Configuration generated") + echo -e "${GREEN}✓${NC} $summary" + fi else - # Run rulesync quietly and capture output for summary - local output - output=$(npx rulesync generate \ - --targets="$targets" \ - --features="rules,ignore,mcp,commands,subagents" \ - --delete 2>&1) - - # Copy extra configs - copy_extra_configs "$verbose" "$targets" - - # Extract the summary line from rulesync output - local summary - summary=$(echo "$output" | grep -o '🎉.*' || echo "Configuration generated") - echo -e "${GREEN}✓${NC} $summary" + echo -e "${YELLOW}Warning: rulesync failed - some configuration may be incomplete${NC}" + if [[ "$verbose" == "true" ]]; then + echo "$output" + echo -e "${YELLOW}This may be due to missing external/prompts (ag-charts-prompts not cloned)${NC}" + else + echo "$output" | grep -i "error" | head -3 || true + fi fi } @@ -346,6 +497,9 @@ main() { esac done + # Setup prompts repository (graceful - doesn't fail on errors) + setup_prompts_repo || true + case $mode in list) print_detected_tools_verbose diff --git a/external/ag-shared/scripts/shard/calculate-pkg-shards.js b/external/ag-shared/scripts/shard/calculate-pkg-shards.js index 9b2e9108c81..b9bcd4c12cd 100755 --- a/external/ag-shared/scripts/shard/calculate-pkg-shards.js +++ b/external/ag-shared/scripts/shard/calculate-pkg-shards.js @@ -25,9 +25,11 @@ if (library === 'grid') { 'ag-charts-angular': 'angular', 'ag-charts-react': 'react', 'ag-charts-vue3': 'vue', + 'ag-charts-server-side': 'server-side', 'angular-package-tests': 'angular', 'react-package-tests': 'react', 'vue-package-tests': 'vue', + 'server-side-package-tests': 'server-side', }; affectedProjectsCmd = 'yarn nx show projects --affected -t pack -t test:package'; } diff --git a/external/ag-shared/scripts/sync-rulesync/sync-rulesync.sh b/external/ag-shared/scripts/sync-rulesync/sync-rulesync.sh new file mode 100755 index 00000000000..c159c6de919 --- /dev/null +++ b/external/ag-shared/scripts/sync-rulesync/sync-rulesync.sh @@ -0,0 +1,509 @@ +#!/usr/bin/env bash +# +# sync-rulesync.sh - Rulesync configuration synchronization +# +# Ensures rulesync patches and configuration are properly set up in consuming +# repositories. Creates symlinks to shared patches and verifies postinstall +# configuration. +# +# Usage: +# ./sync-rulesync.sh # Same as --check +# ./sync-rulesync.sh --check # Verify sync status (dry-run) +# ./sync-rulesync.sh --apply # Apply sync to current repo +# ./sync-rulesync.sh --help # Show help +# +# Exit codes: +# 0 - All checks passed (or successfully applied) +# 1 - Issues found (in --check mode) +# 2 - Failed to apply fixes + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" + +# Shared patch location (relative to repo root) +SHARED_PATCHES_REL="external/ag-shared/prompts/patches" +PATCH_FILE="rulesync+5.2.0.patch" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Track status +ISSUES=0 +FIXED=0 + +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[OK]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" + ((ISSUES++)) || true +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" + ((ISSUES++)) || true +} + +log_fixed() { + echo -e "${GREEN}[FIXED]${NC} $1" + ((FIXED++)) || true +} + +# Check if patches directory exists +check_patches_dir() { + if [[ -d "$REPO_ROOT/patches" ]]; then + log_success "patches/ directory exists" + return 0 + else + log_warn "patches/ directory missing" + return 1 + fi +} + +# Create patches directory +apply_patches_dir() { + if [[ ! -d "$REPO_ROOT/patches" ]]; then + mkdir -p "$REPO_ROOT/patches" + log_fixed "Created patches/ directory" + fi +} + +# Check if patch symlink exists and points to correct location +check_patch_symlink() { + local patch_path="$REPO_ROOT/patches/$PATCH_FILE" + local expected_target="../$SHARED_PATCHES_REL/$PATCH_FILE" + + if [[ ! -e "$patch_path" ]]; then + log_warn "Patch file missing: patches/$PATCH_FILE" + return 1 + fi + + if [[ ! -L "$patch_path" ]]; then + log_warn "Patch file is not a symlink: patches/$PATCH_FILE" + return 1 + fi + + local actual_target + actual_target=$(readlink "$patch_path") + if [[ "$actual_target" != "$expected_target" ]]; then + log_warn "Patch symlink points to wrong location" + log_info " Expected: $expected_target" + log_info " Actual: $actual_target" + return 1 + fi + + log_success "Patch symlink correct: patches/$PATCH_FILE -> $expected_target" + return 0 +} + +# Create or fix patch symlink +apply_patch_symlink() { + local patch_path="$REPO_ROOT/patches/$PATCH_FILE" + local expected_target="../$SHARED_PATCHES_REL/$PATCH_FILE" + local shared_patch="$REPO_ROOT/$SHARED_PATCHES_REL/$PATCH_FILE" + + # Verify shared patch exists + if [[ ! -f "$shared_patch" ]]; then + log_error "Shared patch not found: $SHARED_PATCHES_REL/$PATCH_FILE" + return 1 + fi + + # Remove existing file/symlink if it exists + if [[ -e "$patch_path" ]] || [[ -L "$patch_path" ]]; then + rm "$patch_path" + fi + + # Create symlink + ln -s "$expected_target" "$patch_path" + log_fixed "Created symlink: patches/$PATCH_FILE -> $expected_target" +} + +# Check for stale symlinks in .rulesync/ that point to non-existent targets +# Only checks symlinks pointing to external/ag-shared/ or external/prompts/ +check_stale_rulesync_symlinks() { + local rulesync_dir="$REPO_ROOT/.rulesync" + local stale_count=0 + local stale_links=() + + if [[ ! -d "$rulesync_dir" ]]; then + return 0 + fi + + # Check if external/prompts exists and is valid (not a broken symlink) + local prompts_valid=false + if [[ -d "$REPO_ROOT/external/prompts" ]] && [[ -e "$REPO_ROOT/external/prompts" ]]; then + prompts_valid=true + fi + + # Find all symlinks in .rulesync/ subdirectories + while IFS= read -r -d '' symlink; do + if [[ ! -L "$symlink" ]]; then + continue + fi + + local target + target=$(readlink "$symlink") + + # Only check symlinks pointing to external/ag-shared/ or external/prompts/ + if [[ "$target" != *"external/ag-shared/"* ]] && [[ "$target" != *"external/prompts/"* ]]; then + continue + fi + + # For external/prompts/ symlinks, only flag as stale if external/prompts/ is valid + if [[ "$target" == *"external/prompts/"* ]] && [[ "$prompts_valid" != "true" ]]; then + continue + fi + + # Check if target exists (resolve from symlink's directory) + local symlink_dir + symlink_dir=$(dirname "$symlink") + if [[ ! -e "$symlink_dir/$target" ]]; then + ((stale_count++)) || true + local rel_path="${symlink#$REPO_ROOT/}" + stale_links+=("$rel_path -> $target") + fi + done < <(find "$rulesync_dir" -type l -print0 2>/dev/null) + + if [[ $stale_count -gt 0 ]]; then + log_warn "Found $stale_count stale symlink(s) in .rulesync/" + for link in "${stale_links[@]}"; do + log_info " $link" + done + # Store for apply phase + STALE_SYMLINKS=("${stale_links[@]}") + return 1 + fi + + log_success "No stale symlinks in .rulesync/" + return 0 +} + +# Remove stale symlinks from .rulesync/ +apply_remove_stale_symlinks() { + local rulesync_dir="$REPO_ROOT/.rulesync" + + if [[ ! -d "$rulesync_dir" ]]; then + return 0 + fi + + # Check if external/prompts exists and is valid + local prompts_valid=false + if [[ -d "$REPO_ROOT/external/prompts" ]] && [[ -e "$REPO_ROOT/external/prompts" ]]; then + prompts_valid=true + fi + + local removed=0 + + while IFS= read -r -d '' symlink; do + if [[ ! -L "$symlink" ]]; then + continue + fi + + local target + target=$(readlink "$symlink") + + # Only process symlinks pointing to external/ag-shared/ or external/prompts/ + if [[ "$target" != *"external/ag-shared/"* ]] && [[ "$target" != *"external/prompts/"* ]]; then + continue + fi + + # For external/prompts/ symlinks, only remove if external/prompts/ is valid + if [[ "$target" == *"external/prompts/"* ]] && [[ "$prompts_valid" != "true" ]]; then + continue + fi + + # Check if target exists + local symlink_dir + symlink_dir=$(dirname "$symlink") + if [[ ! -e "$symlink_dir/$target" ]]; then + local rel_path="${symlink#$REPO_ROOT/}" + rm "$symlink" + log_fixed "Removed stale symlink: $rel_path" + ((removed++)) || true + fi + done < <(find "$rulesync_dir" -type l -print0 2>/dev/null) + + return 0 +} + +# Check for missing symlinks in .rulesync/commands/ that should exist based on source files +check_missing_rulesync_symlinks() { + local commands_dir="$REPO_ROOT/.rulesync/commands" + local missing_count=0 + local missing_links=() + + if [[ ! -d "$commands_dir" ]]; then + return 0 + fi + + # Check external/ag-shared/prompts/commands/ (required) + local shared_commands="$REPO_ROOT/external/ag-shared/prompts/commands" + if [[ -d "$shared_commands" ]]; then + # Find all .md files in subdirectories (pattern: subdir/file.md -> subdir-file.md) + while IFS= read -r -d '' source_file; do + local rel_path="${source_file#$shared_commands/}" + local dir_name=$(dirname "$rel_path") + local file_name=$(basename "$rel_path") + + # Compute expected symlink name: dir/file.md -> dir-file.md + local symlink_name="${dir_name}-${file_name}" + local symlink_path="$commands_dir/$symlink_name" + local expected_target="../../external/ag-shared/prompts/commands/$rel_path" + + if [[ ! -e "$symlink_path" ]]; then + ((missing_count++)) || true + missing_links+=("$symlink_name -> $expected_target") + fi + done < <(find "$shared_commands" -mindepth 2 -name "*.md" -type f -print0 2>/dev/null) + fi + + # Check external/prompts/commands/ (optional - only if it exists) + local private_commands="$REPO_ROOT/external/prompts/commands" + if [[ -d "$private_commands" ]] && [[ -e "$private_commands" ]]; then + # Find all top-level .md files (pattern: file.md -> file.md) + while IFS= read -r -d '' source_file; do + local file_name=$(basename "$source_file") + local symlink_path="$commands_dir/$file_name" + local expected_target="../../external/prompts/commands/$file_name" + + if [[ ! -e "$symlink_path" ]]; then + ((missing_count++)) || true + missing_links+=("$file_name -> $expected_target") + fi + done < <(find "$private_commands" -maxdepth 1 -name "*.md" -type f -print0 2>/dev/null) + fi + + if [[ $missing_count -gt 0 ]]; then + log_warn "Found $missing_count missing symlink(s) in .rulesync/commands/" + for link in "${missing_links[@]}"; do + log_info " $link" + done + # Store for apply phase + MISSING_SYMLINKS=("${missing_links[@]}") + return 1 + fi + + log_success "No missing symlinks in .rulesync/commands/" + return 0 +} + +# Create missing symlinks in .rulesync/commands/ +apply_create_missing_symlinks() { + local commands_dir="$REPO_ROOT/.rulesync/commands" + + if [[ ! -d "$commands_dir" ]]; then + mkdir -p "$commands_dir" + fi + + local created=0 + + # Check external/ag-shared/prompts/commands/ (required) + local shared_commands="$REPO_ROOT/external/ag-shared/prompts/commands" + if [[ -d "$shared_commands" ]]; then + while IFS= read -r -d '' source_file; do + local rel_path="${source_file#$shared_commands/}" + local dir_name=$(dirname "$rel_path") + local file_name=$(basename "$rel_path") + + local symlink_name="${dir_name}-${file_name}" + local symlink_path="$commands_dir/$symlink_name" + local expected_target="../../external/ag-shared/prompts/commands/$rel_path" + + if [[ ! -e "$symlink_path" ]]; then + ln -s "$expected_target" "$symlink_path" + log_fixed "Created symlink: .rulesync/commands/$symlink_name" + ((created++)) || true + fi + done < <(find "$shared_commands" -mindepth 2 -name "*.md" -type f -print0 2>/dev/null) + fi + + # Check external/prompts/commands/ (optional) + local private_commands="$REPO_ROOT/external/prompts/commands" + if [[ -d "$private_commands" ]] && [[ -e "$private_commands" ]]; then + while IFS= read -r -d '' source_file; do + local file_name=$(basename "$source_file") + local symlink_path="$commands_dir/$file_name" + local expected_target="../../external/prompts/commands/$file_name" + + if [[ ! -e "$symlink_path" ]]; then + ln -s "$expected_target" "$symlink_path" + log_fixed "Created symlink: .rulesync/commands/$file_name" + ((created++)) || true + fi + done < <(find "$private_commands" -maxdepth 1 -name "*.md" -type f -print0 2>/dev/null) + fi + + return 0 +} + +# Check if postinstall includes patch-package +check_postinstall() { + local package_json="$REPO_ROOT/package.json" + + if [[ ! -f "$package_json" ]]; then + log_error "package.json not found" + return 1 + fi + + # Check for patch-package in postinstall chain + # Handles both direct invocation and npm-run-all patterns (postinstall:patch) + local postinstall_script + local postinstall_patch_script + postinstall_script=$(node -p "try { require('$package_json').scripts?.postinstall || '' } catch { '' }" 2>/dev/null || echo "") + postinstall_patch_script=$(node -p "try { require('$package_json').scripts?.['postinstall:patch'] || '' } catch { '' }" 2>/dev/null || echo "") + + if [[ -z "$postinstall_script" ]]; then + log_warn "No postinstall script found in package.json" + log_info " Add a postinstall script that runs 'patch-package'" + return 1 + fi + + # Direct invocation: postinstall contains patch-package + if [[ "$postinstall_script" == *"patch-package"* ]]; then + log_success "package.json postinstall includes patch-package" + return 0 + fi + + # Indirect via npm-run-all: postinstall runs postinstall:* and postinstall:patch exists + if [[ "$postinstall_script" == *"postinstall:*"* ]]; then + # Check for direct patch-package in postinstall:patch + if [[ "$postinstall_patch_script" == *"patch-package"* ]]; then + log_success "package.json postinstall:patch includes patch-package" + return 0 + fi + # Check for apply-patches.sh script (calls patch-package internally) + if [[ "$postinstall_patch_script" == *"apply-patches.sh"* ]]; then + log_success "package.json postinstall:patch uses apply-patches.sh" + return 0 + fi + fi + + log_warn "postinstall script does not invoke patch-package" + log_info " Current postinstall: $postinstall_script" + log_info " Add 'patch-package' to your postinstall script" + return 1 +} + +# Show help +show_help() { + echo "Usage: sync-rulesync.sh [OPTIONS]" + echo "" + echo "Ensures rulesync patches are properly configured in this repository." + echo "" + echo "Options:" + echo " --check Verify sync status without making changes (default)" + echo " --apply Apply fixes for any issues found" + echo " --help Show this help message" + echo "" + echo "What it checks:" + echo " - patches/ directory exists" + echo " - patches/$PATCH_FILE symlink points to shared location" + echo " - package.json postinstall includes patch-package" + echo " - .rulesync/ has no stale symlinks to external/ag-shared/ or external/prompts/" + echo " - .rulesync/commands/ has all expected symlinks from external/ag-shared/prompts/commands/" + echo " and external/prompts/commands/ (if present)" + echo "" + echo "Shared patch location: $SHARED_PATCHES_REL/$PATCH_FILE" +} + +# Main +main() { + local mode="check" + + # Parse arguments + while [[ $# -gt 0 ]]; do + case $1 in + --check) + mode="check" + shift + ;; + --apply) + mode="apply" + shift + ;; + --help|-h) + show_help + exit 0 + ;; + *) + echo -e "${RED}Unknown option: $1${NC}" + show_help + exit 1 + ;; + esac + done + + echo "" + echo "========================================" + echo " Rulesync Configuration Sync" + echo "========================================" + echo "" + echo "Repository: $REPO_ROOT" + echo "Mode: $mode" + echo "" + + case $mode in + check) + check_patches_dir || true + check_patch_symlink || true + check_postinstall || true + check_stale_rulesync_symlinks || true + check_missing_rulesync_symlinks || true + ;; + apply) + # Check and fix patches directory + if ! check_patches_dir; then + apply_patches_dir + fi + + # Check and fix patch symlink + if ! check_patch_symlink; then + apply_patch_symlink + fi + + # Check postinstall (can only warn, not auto-fix) + check_postinstall || true + + # Check and remove stale symlinks + if ! check_stale_rulesync_symlinks; then + apply_remove_stale_symlinks + fi + + # Check and create missing symlinks + if ! check_missing_rulesync_symlinks; then + apply_create_missing_symlinks + fi + ;; + esac + + echo "" + echo "========================================" + if [[ $mode == "apply" ]] && [[ $FIXED -gt 0 ]]; then + echo -e " ${GREEN}Applied $FIXED fix(es)${NC}" + fi + if [[ $ISSUES -gt 0 ]]; then + echo -e " ${YELLOW}$ISSUES issue(s) found${NC}" + if [[ $mode == "check" ]]; then + echo " Run with --apply to fix" + fi + echo "========================================" + exit 1 + else + echo -e " ${GREEN}All checks passed${NC}" + echo "========================================" + exit 0 + fi +} + +main "$@" From 5502c49c2feec63b84e1ae38071bdd19d307f94d Mon Sep 17 00:00:00 2001 From: Alan Treadway Date: Mon, 19 Jan 2026 09:18:47 +0000 Subject: [PATCH 02/12] Remove local rulesync patch --- patches/rulesync+5.2.0.patch | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 patches/rulesync+5.2.0.patch diff --git a/patches/rulesync+5.2.0.patch b/patches/rulesync+5.2.0.patch deleted file mode 100644 index 4ffa47a86a3..00000000000 --- a/patches/rulesync+5.2.0.patch +++ /dev/null @@ -1,30 +0,0 @@ -diff --git a/node_modules/rulesync/dist/index.js b/node_modules/rulesync/dist/index.js -index 23dce30..e2b3dc0 100755 ---- a/node_modules/rulesync/dist/index.js -+++ b/node_modules/rulesync/dist/index.js -@@ -10595,12 +10595,7 @@ var toolRuleFactories = /* @__PURE__ */ new Map([ - meta: { - extension: "md", - supportsGlobal: false, -- ruleDiscoveryMode: "toon", -- additionalConventions: { -- commands: { commandClass: AgentsmdCommand }, -- subagents: { subagentClass: AgentsmdSubagent }, -- skills: { skillClass: AgentsmdSkill } -- } -+ ruleDiscoveryMode: "auto" - } - } - ], -@@ -10653,10 +10648,7 @@ var toolRuleFactories = /* @__PURE__ */ new Map([ - meta: { - extension: "md", - supportsGlobal: true, -- ruleDiscoveryMode: "toon", -- additionalConventions: { -- subagents: { subagentClass: CodexCliSubagent } -- } -+ ruleDiscoveryMode: "auto" - } - } - ], From 1d9498a6a9173957dcec999485315012473781fc Mon Sep 17 00:00:00 2001 From: Stephen Cooper Date: Mon, 19 Jan 2026 10:14:04 +0000 Subject: [PATCH 03/12] AG-16432 Use merge base instead of latest (#12912) --- .github/workflows/module-size-comparison.yml | 33 ++++++++++++++------ 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/.github/workflows/module-size-comparison.yml b/.github/workflows/module-size-comparison.yml index aee52f38fda..3e5f20f3c34 100644 --- a/.github/workflows/module-size-comparison.yml +++ b/.github/workflows/module-size-comparison.yml @@ -92,22 +92,37 @@ jobs: if: needs.prepare.outputs.is-draft != 'true' outputs: cache-hit: ${{ steps.cache-check.outputs.cache-hit }} - base-sha: ${{ steps.get-sha.outputs.sha }} + base-sha: ${{ steps.get-merge-base.outputs.sha }} steps: - - name: Get base branch SHA - id: get-sha + - name: Checkout to find merge base + uses: actions/checkout@v4 + with: + ref: ${{ needs.prepare.outputs.head-sha }} + fetch-depth: 0 + + - name: Get merge base SHA + id: get-merge-base run: | BASE_REF="${{ needs.prepare.outputs.base-ref }}" - SHA=$(git ls-remote https://github.com/${{ github.repository }} refs/heads/${BASE_REF} | cut -f1) - echo "sha=${SHA}" >> $GITHUB_OUTPUT - echo "Base branch (${BASE_REF}) SHA: ${SHA}" + HEAD_SHA="${{ needs.prepare.outputs.head-sha }}" + + # Fetch the base branch + git fetch origin ${BASE_REF} + + # Find the merge base (common ancestor) + MERGE_BASE=$(git merge-base origin/${BASE_REF} ${HEAD_SHA}) + + echo "sha=${MERGE_BASE}" >> $GITHUB_OUTPUT + echo "Base branch: ${BASE_REF}" + echo "PR head: ${HEAD_SHA}" + echo "Merge base SHA: ${MERGE_BASE}" - name: Check cache for base results id: cache-check uses: actions/cache/restore@v4 with: path: ./base-results.json - key: module-size-base-${{ steps.get-sha.outputs.sha }} + key: module-size-base-${{ steps.get-merge-base.outputs.sha }} lookup-only: true # Build and run module size tests on the PR branch (sharded) @@ -136,7 +151,7 @@ jobs: artifact-prefix: module-size-pr artifact-suffix: ${{ needs.prepare.outputs.pr-number }} - # Build and run module size tests on the base branch (sharded) - only if cache miss + # Build and run module size tests on the merge base (sharded) - only if cache miss module-size-base: name: Module Size Base (${{ matrix.shard }}/${{ strategy.job-total }}) runs-on: ubuntu-latest @@ -156,7 +171,7 @@ jobs: - name: Run module size tests uses: ./.github/actions/module-size-test with: - ref: ${{ needs.prepare.outputs.base-ref }} + ref: ${{ needs.check-base-cache.outputs.base-sha }} shard: ${{ matrix.shard }} total-shards: ${{ strategy.job-total }} artifact-prefix: module-size-base From a165066a8213929e557ea37139beaf87e798f663 Mon Sep 17 00:00:00 2001 From: Guilherme Lopes Date: Mon, 19 Jan 2026 07:20:40 -0300 Subject: [PATCH 04/12] AG-16229 - [formula-cell-editor] - allowed full row edit (#12906) * AG-16229 - [formula-cell-editor] - allowed full row edit * fixed format --- .../ag-grid-community/src/context/context.ts | 3 +- .../src/edit/strategy/fullRowEditStrategy.ts | 5 + .../src/interfaces/formulas.ts | 7 +- packages/ag-grid-community/src/main.ts | 1 + .../src/formula/editor/formulaCellEditor.ts | 34 ++++ .../src/formula/formulaInputManagerService.ts | 69 ++++++++ .../src/formula/formulaModule.ts | 3 +- .../src/formula/formulaService.ts | 37 +---- .../src/widgets/agFormulaInputField.ts | 92 +++++++++-- .../widgets/formulaInputRangeSyncFeature.ts | 154 ++++++++++++------ 10 files changed, 308 insertions(+), 97 deletions(-) create mode 100644 packages/ag-grid-enterprise/src/formula/formulaInputManagerService.ts diff --git a/packages/ag-grid-community/src/context/context.ts b/packages/ag-grid-community/src/context/context.ts index 945ed2f4163..9baee1870fd 100644 --- a/packages/ag-grid-community/src/context/context.ts +++ b/packages/ag-grid-community/src/context/context.ts @@ -46,7 +46,7 @@ import type { RowNodeBlockLoader } from '../infiniteRowModel/rowNodeBlockLoader' import type { IChartService } from '../interfaces/IChartService'; import type { IRangeService } from '../interfaces/IRangeService'; import type { EditStrategyType } from '../interfaces/editStrategyType'; -import type { IFormulaDataService, IFormulaService } from '../interfaces/formulas'; +import type { IFormulaDataService, IFormulaInputManagerService, IFormulaService } from '../interfaces/formulas'; import type { IAdvancedFilterService } from '../interfaces/iAdvancedFilterService'; import type { IAggColumnNameService } from '../interfaces/iAggColumnNameService'; import type { IAggFuncService } from '../interfaces/iAggFuncService'; @@ -372,6 +372,7 @@ interface CoreBeanCollection groupHierarchyColSvc?: IGroupHierarchyColService; formulaDataSvc?: IFormulaDataService; formula?: IFormulaService; + formulaInputManager?: IFormulaInputManagerService; } export type BeanCollection = CoreBeanCollection & { diff --git a/packages/ag-grid-community/src/edit/strategy/fullRowEditStrategy.ts b/packages/ag-grid-community/src/edit/strategy/fullRowEditStrategy.ts index 85bce5cd1ba..182cd6b1668 100644 --- a/packages/ag-grid-community/src/edit/strategy/fullRowEditStrategy.ts +++ b/packages/ag-grid-community/src/edit/strategy/fullRowEditStrategy.ts @@ -165,6 +165,11 @@ export class FullRowEditStrategy extends BaseEditStrategy { return; } + // allow range selection while editing without ending the row edit. + if (this.beans.editSvc?.isRangeSelectionEnabledWhileEditing()) { + return; + } + const prevCell = _getCellCtrl(this.beans, prev); const isBlock = this.gos.get('invalidEditValueMode') === 'block'; diff --git a/packages/ag-grid-community/src/interfaces/formulas.ts b/packages/ag-grid-community/src/interfaces/formulas.ts index 940dbd7d2b2..3c4c0938379 100644 --- a/packages/ag-grid-community/src/interfaces/formulas.ts +++ b/packages/ag-grid-community/src/interfaces/formulas.ts @@ -73,7 +73,6 @@ export interface IFormulaDataService extends Bean { export interface IFormulaService extends Bean { active: boolean; - activeEditor: number | null; isFormula(value: unknown): value is `=${string}`; setFormulasActive(cols: ColumnCollections): void; resolveValue(col: AgColumn, row: RowNode): unknown; @@ -91,3 +90,9 @@ export interface IFormulaService extends Bean { getFunction(name: string): ((params: FormulaFunctionParams) => unknown) | undefined; getFunctionNames(): string[]; } + +export interface IFormulaInputManagerService extends Bean { + registerActiveEditor(editorId: number, onDeactivate: () => void): boolean; + unregisterActiveEditor(editorId: number, onDeactivate: () => void): void; + isActiveEditor(editorId: number): boolean; +} diff --git a/packages/ag-grid-community/src/main.ts b/packages/ag-grid-community/src/main.ts index 704548e777f..3749d552d9e 100644 --- a/packages/ag-grid-community/src/main.ts +++ b/packages/ag-grid-community/src/main.ts @@ -905,6 +905,7 @@ export { GetFormulaParams, IFormulaDataService, IFormulaService, + IFormulaInputManagerService, RangeParam, SetFormulaParams, ValueParam, diff --git a/packages/ag-grid-enterprise/src/formula/editor/formulaCellEditor.ts b/packages/ag-grid-enterprise/src/formula/editor/formulaCellEditor.ts index f12ef074d0b..ffa1b9fd56c 100644 --- a/packages/ag-grid-enterprise/src/formula/editor/formulaCellEditor.ts +++ b/packages/ag-grid-enterprise/src/formula/editor/formulaCellEditor.ts @@ -17,6 +17,9 @@ export class FormulaCellEditor extends AgAbstractCellEditor { this.eEditor = formulaInputField; formulaInputField.addCss('ag-cell-editor'); this.appendChild(formulaInputField); + this.addManagedElementListeners(formulaInputField.getContentElement(), { + keydown: (event: KeyboardEvent) => this.onFormulaInputKeyDown(event, params.onKeyDown), + }); const { eventKey, cellStartedEdit } = params; @@ -41,6 +44,37 @@ export class FormulaCellEditor extends AgAbstractCellEditor { this.eEditor.setValue(initialValue, true); } + private onFormulaInputKeyDown(event: KeyboardEvent, onKeyDown: (event: KeyboardEvent) => void) { + const { key } = event; + if (key !== KeyCode.TAB || event.defaultPrevented) { + return; + } + const { focusSvc } = this.beans; + const prevFocus = focusSvc?.getFocusedCell(); + + // prevent range sync from reacting to the focus change caused by tab navigation. + this.eEditor.withSelectionChangeHandlingSuppressed(() => { + onKeyDown?.(event); + }); + + const nextFocus = focusSvc?.getFocusedCell(); + let focusChanged = false; + if (prevFocus && nextFocus) { + const { rowIndex: prevRowIndex, rowPinned: prevRowPinned, column: prevColumn } = prevFocus; + const { rowIndex: nextRowIndex, rowPinned: nextRowPinned, column: nextColumn } = nextFocus; + focusChanged = + prevRowIndex !== nextRowIndex || prevRowPinned !== nextRowPinned || prevColumn !== nextColumn; + } + + const { defaultPrevented } = event; + if (defaultPrevented || focusChanged) { + // stop contenteditable from inserting a tab when the grid handled navigation. + event.preventDefault(); + } + + event.stopPropagation(); + } + private getStartValue(params: ICellEditorParams): string | null | undefined { const { value } = params; return value?.toString() ?? value; diff --git a/packages/ag-grid-enterprise/src/formula/formulaInputManagerService.ts b/packages/ag-grid-enterprise/src/formula/formulaInputManagerService.ts new file mode 100644 index 00000000000..ba9ef39629e --- /dev/null +++ b/packages/ag-grid-enterprise/src/formula/formulaInputManagerService.ts @@ -0,0 +1,69 @@ +import type { IFormulaInputManagerService, NamedBean } from 'ag-grid-community'; +import { BeanStub } from 'ag-grid-community'; + +import type { + RangeSelectionExtension, + RangeSelectionExtensionRegistry, +} from '../rangeSelection/rangeSelectionExtensions'; + +export class FormulaInputManagerService + extends BeanStub + implements IFormulaInputManagerService, NamedBean, RangeSelectionExtension +{ + public readonly beanName = 'formulaInputManager' as const; + + private activeEditor: number | null = null; + private activeEditorDeactivate: (() => void) | null = null; + + public postConstruct(): void { + this.registerRangeSelectionExtension(); + } + + public registerActiveEditor(editorId: number, onDeactivate: () => void): boolean { + if (this.activeEditor === editorId && this.activeEditorDeactivate === onDeactivate) { + return false; + } + + // only one editor should sync ranges at a time when multiple editors are visible. + const previousDeactivate = this.activeEditorDeactivate; + if (previousDeactivate && previousDeactivate !== onDeactivate) { + previousDeactivate(); + } + + this.activeEditor = editorId; + this.activeEditorDeactivate = onDeactivate; + return true; + } + + public unregisterActiveEditor(editorId: number, onDeactivate: () => void): void { + if (this.activeEditor === editorId && this.activeEditorDeactivate === onDeactivate) { + this.activeEditor = null; + this.activeEditorDeactivate = null; + } + } + + public isActiveEditor(editorId: number): boolean { + return this.activeEditor === editorId; + } + + public shouldSuppressRangeSelection(eventTarget: EventTarget | null): boolean { + const target = eventTarget as HTMLElement | null; + if (!target?.closest) { + return false; + } + // when a formula editor is active, suppress range selection for all editors to avoid stealing focus. + if (this.activeEditor != null) { + return !!target.closest('.ag-cell-editor'); + } + return !!target.closest('.ag-formula-input-field'); + } + + private registerRangeSelectionExtension(): void { + const rangeSvc = this.beans.rangeSvc as RangeSelectionExtensionRegistry | undefined; + if (!rangeSvc) { + return; + } + rangeSvc.registerRangeSelectionExtension(this); + this.addDestroyFunc(() => rangeSvc.unregisterRangeSelectionExtension?.(this)); + } +} diff --git a/packages/ag-grid-enterprise/src/formula/formulaModule.ts b/packages/ag-grid-enterprise/src/formula/formulaModule.ts index ec84afdaf77..8df019d0b2a 100644 --- a/packages/ag-grid-enterprise/src/formula/formulaModule.ts +++ b/packages/ag-grid-enterprise/src/formula/formulaModule.ts @@ -5,6 +5,7 @@ import { VERSION } from '../version'; import { FormulaCellEditor } from './editor/formulaCellEditor'; import { formulaCSS } from './formula.css-GENERATED'; import { FormulaDataService } from './formulaDataService'; +import { FormulaInputManagerService } from './formulaInputManagerService'; import { FormulaService } from './formulaService'; /** @@ -14,7 +15,7 @@ export const FormulaModule: _ModuleWithoutApi = { moduleName: 'Formula', version: VERSION, userComponents: { agFormulaCellEditor: FormulaCellEditor }, - beans: [FormulaService, FormulaDataService], + beans: [FormulaService, FormulaDataService, FormulaInputManagerService], dependsOn: [RowNumbersModule], css: [formulaCSS], }; diff --git a/packages/ag-grid-enterprise/src/formula/formulaService.ts b/packages/ag-grid-enterprise/src/formula/formulaService.ts index a614e8e3737..892d893e4b8 100644 --- a/packages/ag-grid-enterprise/src/formula/formulaService.ts +++ b/packages/ag-grid-enterprise/src/formula/formulaService.ts @@ -9,10 +9,6 @@ import type { } from 'ag-grid-community'; import { BeanStub, _convertColumnEventSourceType, _isExpressionString, _warn } from 'ag-grid-community'; -import type { - RangeSelectionExtension, - RangeSelectionExtensionRegistry, -} from '../rangeSelection/rangeSelectionExtensions'; import { parseFormula } from './ast/parsers'; import { serializeFormula } from './ast/serializer'; import type { FormulaNode } from './ast/utils'; @@ -96,7 +92,7 @@ interface FormulaFrame { ast: FormulaNode; unresolvedDepIterator: Generator; } -export class FormulaService extends BeanStub implements IFormulaService, NamedBean, RangeSelectionExtension { +export class FormulaService extends BeanStub implements IFormulaService, NamedBean { public readonly beanName = 'formula' as const; /** Cache: row -> (column -> CellFormula) */ @@ -109,9 +105,6 @@ export class FormulaService extends BeanStub implements IFormulaService, NamedBe private supportedOperations: Map unknown>; private functionNames: string[] | null = null; - // Track the active editor instance per grid/cell to avoid overlapping syncs on editor restarts. - public activeEditor: number | null = null; - public active = false; public setFormulasActive(cols: _ColumnCollections): void { @@ -159,7 +152,6 @@ export class FormulaService extends BeanStub implements IFormulaService, NamedBe public postConstruct(): void { this.setupFunctions(); - this.registerRangeSelectionExtension(); const refreshFormulas = () => { if (this.active) { @@ -178,7 +170,7 @@ export class FormulaService extends BeanStub implements IFormulaService, NamedBe const { colModel } = this.beans; const formulaColumnsPresent = colModel.cols?.list.some((col) => col.isAllowFormula()); if (formulaColumnsPresent) { - this.beans.colModel.refreshAll(_convertColumnEventSourceType(e.source)); + colModel.refreshAll(_convertColumnEventSourceType(e.source)); } }); @@ -191,19 +183,6 @@ export class FormulaService extends BeanStub implements IFormulaService, NamedBe }); } - public shouldSuppressRangeSelection(eventTarget: EventTarget | null): boolean { - return !!(eventTarget as HTMLElement | null)?.closest?.('.ag-formula-input-field'); - } - - private registerRangeSelectionExtension(): void { - const rangeSvc = this.beans.rangeSvc as RangeSelectionExtensionRegistry | undefined; - if (!rangeSvc) { - return; - } - rangeSvc.registerRangeSelectionExtension(this); - this.addDestroyFunc(() => rangeSvc.unregisterRangeSelectionExtension?.(this)); - } - public updateFormulaByOffset(params: { value: string; rowDelta?: number; @@ -211,13 +190,14 @@ export class FormulaService extends BeanStub implements IFormulaService, NamedBe useRefFormat?: boolean; }): string { const { value, rowDelta = 0, columnDelta = 0, useRefFormat = true } = params; + const { beans } = this; try { const unsafe = !useRefFormat; - const ast = parseFormula(this.beans, value, unsafe); - shiftNode(this.beans, ast, rowDelta, columnDelta, unsafe); + const ast = parseFormula(beans, value, unsafe); + shiftNode(beans, ast, rowDelta, columnDelta, unsafe); // Serialize back to a formula string (REF format) - return serializeFormula(this.beans, ast, /*useRefFormat*/ useRefFormat, unsafe); + return serializeFormula(beans, ast, /*useRefFormat*/ useRefFormat, unsafe); } catch { return value; } @@ -337,9 +317,10 @@ export class FormulaService extends BeanStub implements IFormulaService, NamedBe * @returns null if the formula is invalid. */ public normaliseFormula(value: string, shorthand: boolean = false): string | null { + const { beans } = this; try { - const parsedAST = parseFormula(this.beans, value); - const serialized = serializeFormula(this.beans, parsedAST, !shorthand, false); + const parsedAST = parseFormula(beans, value); + const serialized = serializeFormula(beans, parsedAST, !shorthand, false); return serialized; } catch { return null; diff --git a/packages/ag-grid-enterprise/src/widgets/agFormulaInputField.ts b/packages/ag-grid-enterprise/src/widgets/agFormulaInputField.ts index 558a1e4ecb4..403f714d2fe 100644 --- a/packages/ag-grid-enterprise/src/widgets/agFormulaInputField.ts +++ b/packages/ag-grid-enterprise/src/widgets/agFormulaInputField.ts @@ -6,7 +6,7 @@ import type { GridOptionsService, GridOptionsWithDefaults, } from 'ag-grid-community'; -import { AgContentEditableField, _createElement, _getDocument, _getWindow } from 'ag-grid-community'; +import { AgContentEditableField, _createElement, _getDocument, _getWindow, _placeCaretAtEnd } from 'ag-grid-community'; import { agAutocompleteCSS } from '../advancedFilter/autocomplete/agAutocomplete.css-GENERATED'; import { getRefTokenMatches } from '../formula/refUtils'; @@ -47,6 +47,10 @@ export class AgFormulaInputField extends AgContentEditableField< private lastTokenRef?: string; private rangeSyncFeature?: FormulaInputRangeSyncFeature; private autocompleteFeature?: FormulaInputAutocompleteFeature; + // record mouse focus so we don't jump the caret to the end after a click. + private focusFromMouseTime: number | null = null; + // skip auto-caret placement when we are restoring a caret programmatically. + private suppressNextFocusCaretPlacement = false; // fallback color assignment per ref when a token index is unavailable. private readonly formulaColorByRef = new Map(); @@ -61,12 +65,15 @@ export class AgFormulaInputField extends AgContentEditableField< public override postConstruct(): void { super.postConstruct(); + this.rangeSyncFeature = this.createManagedBean(new FormulaInputRangeSyncFeature(this)); + this.autocompleteFeature = this.createManagedBean(new FormulaInputAutocompleteFeature(this)); + this.addManagedElementListeners(this.getContentElement(), { input: this.onContentInput.bind(this), + focus: this.onContentFocus.bind(this), + blur: this.onContentBlur.bind(this), + mousedown: this.onContentMouseDown.bind(this), }); - - this.rangeSyncFeature = this.createManagedBean(new FormulaInputRangeSyncFeature(this)); - this.autocompleteFeature = this.createManagedBean(new FormulaInputAutocompleteFeature(this)); } public override setValue(value?: string | null, silent?: boolean): this { @@ -138,6 +145,15 @@ export class AgFormulaInputField extends AgContentEditableField< restoreCaret(this.beans, contentElement, targetCaret); } + public withSelectionChangeHandlingSuppressed(action: () => void): void { + if (!this.rangeSyncFeature) { + action(); + return; + } + // proxy to the range sync feature so tab navigation doesn't rewrite formulas. + this.rangeSyncFeature.withSelectionChangeHandlingSuppressed(action); + } + public getColorIndexForRef(ref: string): number | null { if (!shouldUseTokenColors(this.beans)) { return null; @@ -225,6 +241,39 @@ export class AgFormulaInputField extends AgContentEditableField< this.rangeSyncFeature?.onValueUpdated(serialized, hasFormulaPrefix); } + private onContentFocus(): void { + this.rangeSyncFeature?.setEditorActive(true); + // avoid overriding caret placement after token updates. + if (this.suppressNextFocusCaretPlacement) { + this.suppressNextFocusCaretPlacement = false; + return; + } + const { focusFromMouseTime } = this; + const focusFromMouse = focusFromMouseTime != null; + this.focusFromMouseTime = null; + if (focusFromMouse) { + return; + } + // keyboard focus should land at the end for fast append editing. + _placeCaretAtEnd(this.beans, this.getContentElement()); + } + + private onContentBlur(event: FocusEvent): void { + this.focusFromMouseTime = null; + const nextTarget = event.relatedTarget as HTMLElement | null; + // only deactivate when moving to another cell editor inside the grid. + const editorTarget = nextTarget?.closest('.ag-cell-editor'); + const cellTarget = nextTarget?.closest('.ag-cell'); + if (!nextTarget || this.getGui().contains(nextTarget) || !editorTarget || !cellTarget) { + return; + } + this.rangeSyncFeature?.deactivateForFocusLoss(); + } + + private onContentMouseDown(): void { + this.focusFromMouseTime = Date.now(); + } + public insertOrReplaceToken(ref: string, isNew: boolean): TokenInsertResult { const offsets = this.getTokenInsertOffsets(isNew); @@ -302,12 +351,14 @@ export class AgFormulaInputField extends AgContentEditableField< return { action: 'insert', previousRef, tokenIndex }; } + const { valueOffset } = caretOffsets; // if the caret is inside/adjacent to a token, replace that token. - const tokenMatch = getTokenMatchAtOffset(value, caretOffsets.valueOffset); + const tokenMatch = getTokenMatchAtOffset(value, valueOffset); if (tokenMatch) { + const { end: tokenEnd, ref: tokenRef } = tokenMatch; // if the user is completing a partial range like "A1:", keep the range and insert the end ref. - if (tokenMatch.ref.endsWith(':') && caretOffsets.valueOffset === tokenMatch.end) { + if (tokenRef.endsWith(':') && valueOffset === tokenEnd) { const { previousRef, tokenIndex } = this.insertOrReplaceToken(ref, true); return { action: 'insert', previousRef, tokenIndex }; } @@ -316,7 +367,7 @@ export class AgFormulaInputField extends AgContentEditableField< } // only insert new refs after operator-like chars; otherwise we end the edit on click. - if (!shouldInsertTokenAtOffset(value, caretOffsets.valueOffset)) { + if (!shouldInsertTokenAtOffset(value, valueOffset)) { return { action: 'none' }; } @@ -331,12 +382,17 @@ export class AgFormulaInputField extends AgContentEditableField< this.currentValue.length; const caret = caretBase + (this.lastTokenValueLength ?? 0); this.selectionCaretOffset = null; + // avoid onFocus forcing the caret to the end while we restore its position. + this.suppressNextFocusCaretPlacement = true; setTimeout(() => { if (!this.isAlive()) { return; } this.getContentElement().focus({ preventScroll: true }); + if (_getDocument(this.beans).activeElement === this.getContentElement()) { + this.suppressNextFocusCaretPlacement = false; + } restoreCaret(this.beans, this.getContentElement(), caret); }); } @@ -401,8 +457,9 @@ export class AgFormulaInputField extends AgContentEditableField< ): { caretOffset: number; valueOffset: number } | null { // snapshot the caret position in both caret units and raw string offsets. const { beans } = this; + const { useCachedCaret, useCachedValueOffset } = options; const contentElement = this.getContentElement(); - const caretOffset = options.useCachedCaret + const caretOffset = useCachedCaret ? this.selectionCaretOffset ?? getCaretOffset(beans, contentElement, value) ?? this.currentValue.length : getCaretOffset(beans, contentElement, value); @@ -411,7 +468,7 @@ export class AgFormulaInputField extends AgContentEditableField< } const valueOffset = - options.useCachedValueOffset && this.lastTokenValueOffset != null + useCachedValueOffset && this.lastTokenValueOffset != null ? this.lastTokenValueOffset : this.getValueOffsetFromCaret(caretOffset); @@ -481,13 +538,16 @@ export class AgFormulaInputField extends AgContentEditableField< caret: number; updateTracking?: () => void; }): void { - this.updateFormulaColorsFromValue(params.nextValue); + const { currentValue, nextValue, caret } = params; + this.updateFormulaColorsFromValue(nextValue); + params.updateTracking?.(); - this.setEditorValue(params.nextValue); + + this.setEditorValue(nextValue); this.renderFormula({ - currentValue: params.currentValue, - value: params.nextValue, - caret: params.caret, + currentValue, + value: nextValue, + caret, }); this.dispatchValueChanged(); this.autocompleteFeature?.onFormulaValueUpdated(); @@ -551,8 +611,8 @@ export class AgFormulaInputField extends AgContentEditableField< // Token/range color helpers const shouldUseTokenColors = (beans: BeanCollection): boolean => { - const { editSvc, rangeSvc } = beans; - const canCreateRanges = !!rangeSvc && !!editSvc?.isRangeSelectionEnabledWhileEditing?.(); + const { gos, rangeSvc } = beans; + const canCreateRanges = !!rangeSvc && !!gos.get('cellSelection'); return canCreateRanges; }; diff --git a/packages/ag-grid-enterprise/src/widgets/formulaInputRangeSyncFeature.ts b/packages/ag-grid-enterprise/src/widgets/formulaInputRangeSyncFeature.ts index 70684765c14..273fdc1fd7f 100644 --- a/packages/ag-grid-enterprise/src/widgets/formulaInputRangeSyncFeature.ts +++ b/packages/ag-grid-enterprise/src/widgets/formulaInputRangeSyncFeature.ts @@ -31,6 +31,17 @@ export class FormulaInputRangeSyncFeature extends BeanStub { private ignoreNextRangeEvent = false; // avoids a value update loop when we re-render on enabling range selection. private skipNextValueUpdate = false; + // suppress selection handling while keyboard navigation updates the focus range. + private suppressSelectionChangeHandling = false; + + // keep a stable callback so the input manager can deactivate the previous editor. + private readonly handleEditorDeactivated = () => { + this.rangeSelectionEnabled = false; + this.suppressRangeEvents = false; + this.ignoreNextRangeEvent = false; + this.skipNextValueUpdate = false; + this.clearTrackedRanges(true); + }; constructor(private readonly field: AgFormulaInputField) { super(); @@ -73,40 +84,80 @@ export class FormulaInputRangeSyncFeature extends BeanStub { this.editingColumn = column; this.editingRowIndex = rowIndex ?? undefined; this.editingCellRef = editingCellRef; - this.registerActiveEditor(); + } + + public setEditorActive(active: boolean): void { + if (active) { + // focus decides which editor owns range syncing when multiple editors are open. + this.registerActiveEditor(); + return; + } + this.unregisterActiveEditor(); + } + + public withSelectionChangeHandlingSuppressed(action: () => void): void { + // avoid re-writing the formula when the grid updates range selection during navigation. + const previous = this.suppressSelectionChangeHandling; + this.suppressSelectionChangeHandling = true; + try { + action(); + } finally { + this.suppressSelectionChangeHandling = previous; + } + } + + public deactivateForFocusLoss(): void { + if (!this.isActiveEditor()) { + return; + } + // drop range sync when focus moves to a non-formula editor. + this.handleEditorDeactivated(); + this.beans.editSvc?.disableRangeSelectionWhileEditing?.(); + this.unregisterActiveEditor(); } private registerActiveEditor(): void { const fieldId = this.field.getCompId(); - const { formula } = this.beans; + const { formulaInputManager } = this.beans; - if (!formula) { + if (!formulaInputManager) { return; } - if (formula.activeEditor !== fieldId) { - formula.activeEditor = fieldId; + const becameActive = formulaInputManager.registerActiveEditor(fieldId, this.handleEditorDeactivated); + + if (!becameActive) { + return; } + + // reset tracking so the new active editor rebuilds its range state cleanly. + this.rangeSelectionEnabled = false; + this.suppressRangeEvents = false; + this.ignoreNextRangeEvent = false; + this.skipNextValueUpdate = false; + this.clearTrackedRanges(false); + + const value = this.field.getCurrentValue(); + const hasFormulaPrefix = value.trimStart().startsWith('='); + this.onValueUpdated(value, hasFormulaPrefix); } private unregisterActiveEditor(): void { const fieldId = this.field.getCompId(); - const { formula } = this.beans; + const { formulaInputManager } = this.beans; - if (!formula) { + if (!formulaInputManager) { return; } - if (formula.activeEditor === fieldId) { - formula.activeEditor = null; - } + formulaInputManager.unregisterActiveEditor(fieldId, this.handleEditorDeactivated); } private isActiveEditor(): boolean { const fieldId = this.field.getCompId(); - const { formula } = this.beans; + const { formulaInputManager } = this.beans; - return !!formula && formula.activeEditor === fieldId; + return !!formulaInputManager && formulaInputManager.isActiveEditor(fieldId); } private getTrackedRefCount(ref: string): number { @@ -257,12 +308,14 @@ export class FormulaInputRangeSyncFeature extends BeanStub { const desiredByRef = new Map(); for (const token of refTokens) { - if (token.ref === this.editingCellRef) { + const { ref, index } = token; + if (ref === this.editingCellRef) { continue; } - const list = desiredByRef.get(token.ref) ?? []; - list.push(token.index); - desiredByRef.set(token.ref, list); + + const list = desiredByRef.get(ref) ?? []; + list.push(index); + desiredByRef.set(ref, list); } for (const ref of Array.from(this.trackedRangeRefs.keys())) { @@ -358,17 +411,23 @@ export class FormulaInputRangeSyncFeature extends BeanStub { ) { return; } + if (this.ignoreNextRangeEvent) { this.ignoreNextRangeEvent = false; return; } + if (this.suppressSelectionChangeHandling) { + return; + } + + const { finished, started } = event; const liveRanges = this.getLiveRanges(); const latestRange = liveRanges.length ? _last(liveRanges) : null; const latestRef = latestRange ? rangeToRef(this.beans, latestRange) : null; const hasInsertCandidate = !!latestRange && !this.trackedRanges.has(latestRange) && !!latestRef && latestRef !== this.editingCellRef; - const shouldInsert = event.finished && (event.started || hasInsertCandidate); + const shouldInsert = finished && (started || hasInsertCandidate); // re-tag ranges if their colors are out of sync with the formula tokens. const reTagged = this.ensureTrackedRangeColors(); @@ -380,7 +439,7 @@ export class FormulaInputRangeSyncFeature extends BeanStub { return; } - if (event.started || hasInsertCandidate) { + if (started || hasInsertCandidate) { // remember caret so we can restore it after any selection-driven edits. this.field.rememberCaret(); } @@ -433,7 +492,7 @@ export class FormulaInputRangeSyncFeature extends BeanStub { return; } - if (!event.started && !event.finished) { + if (!started && !finished) { // drag updates should rewrite the active token as the range grows/shrinks. const { previousRef, tokenIndex } = this.field.insertOrReplaceToken(ref, false); this.tagLatestRangeForRef(ref, tokenIndex); @@ -444,7 +503,7 @@ export class FormulaInputRangeSyncFeature extends BeanStub { this.tagLatestRangeForRef(ref); - if (event.finished) { + if (finished) { this.field.restoreCaretAfterToken(); this.refocusEditingCell(); } @@ -474,12 +533,7 @@ export class FormulaInputRangeSyncFeature extends BeanStub { return; } - if (isNew) { - this.addTrackedRef(ref); - return; - } - - if (!previousRef) { + if (isNew || !previousRef) { this.addTrackedRef(ref); return; } @@ -592,7 +646,8 @@ export class FormulaInputRangeSyncFeature extends BeanStub { continue; } - const tokenColorIndex = this.field.getColorIndexForToken(tracked?.tokenIndex ?? null); + const tokenIndex = tracked?.tokenIndex ?? null; + const tokenColorIndex = this.field.getColorIndexForToken(tokenIndex); const inferredColorIndex = getRangeColorIndexFromClass(range.colorClass); const colorIndex = tokenColorIndex ?? @@ -609,7 +664,7 @@ export class FormulaInputRangeSyncFeature extends BeanStub { } if (!this.trackedRanges.has(range)) { - this.trackRange(range, ref, tracked?.tokenIndex ?? null); + this.trackRange(range, ref, tokenIndex); } } @@ -623,7 +678,7 @@ export class FormulaInputRangeSyncFeature extends BeanStub { } const value = this.field.getCurrentValue(); - const tokens = getRefTokensFromText(value).filter((token) => token.ref !== this.editingCellRef); + const tokens = getRefTokensFromText(value).filter(({ ref }) => ref !== this.editingCellRef); if (!tokens.length) { return false; } @@ -642,7 +697,8 @@ export class FormulaInputRangeSyncFeature extends BeanStub { const pendingRemovals = new Map(); for (const token of tokens) { - pendingRemovals.set(token.ref, (pendingRemovals.get(token.ref) ?? 0) + 1); + const { ref } = token; + pendingRemovals.set(ref, (pendingRemovals.get(ref) ?? 0) + 1); } for (const [ref, tokenCount] of Array.from(pendingRemovals.entries())) { const liveCount = liveCounts.get(ref) ?? 0; @@ -663,11 +719,12 @@ export class FormulaInputRangeSyncFeature extends BeanStub { if (liveSet.has(range)) { continue; } - const remaining = pendingRemovals.get(tracked.ref) ?? 0; + const { ref } = tracked; + const remaining = pendingRemovals.get(ref) ?? 0; if (remaining <= 0) { continue; } - pendingRemovals.set(tracked.ref, remaining - 1); + pendingRemovals.set(ref, remaining - 1); removals.push({ range, tracked }); } @@ -679,9 +736,10 @@ export class FormulaInputRangeSyncFeature extends BeanStub { let removed = false; for (const { range, tracked } of removals) { - removed = this.field.removeTokenRef(tracked.ref, tracked.tokenIndex ?? null) || removed; + const { ref, tokenIndex } = tracked; + removed = this.field.removeTokenRef(ref, tokenIndex ?? null) || removed; this.trackedRanges.delete(range); - this.removeTrackedRef(tracked.ref); + this.removeTrackedRef(ref); } if (removed) { @@ -710,12 +768,13 @@ export class FormulaInputRangeSyncFeature extends BeanStub { private refocusEditingCell(): void { // keep focus on the edited cell so keyboard editing continues. const { focusSvc } = this.beans; - if (!focusSvc || this.editingColumn == null || this.editingRowIndex == null) { + const { editingColumn, editingRowIndex } = this; + if (!focusSvc || editingColumn == null || editingRowIndex == null) { return; } focusSvc.setFocusedCell({ - column: this.editingColumn as any, - rowIndex: this.editingRowIndex, + column: editingColumn, + rowIndex: editingRowIndex, rowPinned: null, preventScrollOnBrowserFocus: true, }); @@ -746,10 +805,8 @@ export class FormulaInputRangeSyncFeature extends BeanStub { if (tokenIndex != null) { let removed = false; for (const [range, tracked] of Array.from(this.trackedRanges.entries())) { - if (tracked.ref !== ref) { - continue; - } - if (tracked.tokenIndex !== tokenIndex) { + const { ref: trackedRef, tokenIndex: trackedTokenIndex } = tracked; + if (trackedRef !== ref || trackedTokenIndex !== tokenIndex) { continue; } this.removeTrackedRange(range); @@ -792,7 +849,7 @@ export class FormulaInputRangeSyncFeature extends BeanStub { continue; } - const previousRef = tracked.ref; + const { ref: previousRef, tokenIndex } = tracked; const nextRef = rangeToRef(this.beans, range); const normalisedPrevious = this.normaliseRefForComparison(previousRef); const normalisedNext = this.normaliseRefForComparison(nextRef); @@ -800,22 +857,19 @@ export class FormulaInputRangeSyncFeature extends BeanStub { continue; } - const tokenColorIndex = this.field.getColorIndexForToken(tracked.tokenIndex ?? null); + const { colorClass } = range; + const tokenColorIndex = this.field.getColorIndexForToken(tokenIndex ?? null); const colorIndex = tokenColorIndex ?? - this.field.moveColorToRef( - previousRef, - nextRef, - getRangeColorIndexFromClass(range.colorClass) ?? undefined - ); + this.field.moveColorToRef(previousRef, nextRef, getRangeColorIndexFromClass(colorClass) ?? undefined); - const replacedIndex = this.field.replaceTokenRef(previousRef, nextRef, colorIndex, tracked.tokenIndex); + const replacedIndex = this.field.replaceTokenRef(previousRef, nextRef, colorIndex, tokenIndex); if (replacedIndex == null) { continue; } this.tagRangeColor(range, nextRef, colorIndex); - this.trackRange(range, nextRef, replacedIndex ?? tracked.tokenIndex ?? null); + this.trackRange(range, nextRef, replacedIndex ?? tokenIndex ?? null); updated = true; } From 80be9fc64edb90c6c5cb8e6d534821ca5be4b765 Mon Sep 17 00:00:00 2001 From: Guilherme Lopes Date: Mon, 19 Jan 2026 07:21:21 -0300 Subject: [PATCH 05/12] AG-16440 - [formula-cell-editor] - fixed dark color scheme (#12903) --- .../styles/src/internal/base/parts/_widgets.scss | 2 +- .../internal/themes/alpine/_alpine-variables.scss | 15 +++++++++++++++ .../internal/themes/balham/_balham-variables.scss | 15 +++++++++++++++ .../themes/material/_material-variables.scss | 15 +++++++++++++++ .../internal/themes/quartz/_quartz-variables.scss | 15 +++++++++++++++ .../theming/parts/color-scheme/color-schemes.ts | 8 ++++++++ .../src/widgets/agFormulaInputField.css | 2 +- 7 files changed, 70 insertions(+), 2 deletions(-) diff --git a/community-modules/styles/src/internal/base/parts/_widgets.scss b/community-modules/styles/src/internal/base/parts/_widgets.scss index 8d7ac0bddb1..8b8c96552c6 100644 --- a/community-modules/styles/src/internal/base/parts/_widgets.scss +++ b/community-modules/styles/src/internal/base/parts/_widgets.scss @@ -716,7 +716,7 @@ } .ag-formula-token-color-7 { - color: var(--ag-formula-token-6-color); + color: var(--ag-formula-token-7-color); } .ag-formula-range-color-1 { diff --git a/community-modules/styles/src/internal/themes/alpine/_alpine-variables.scss b/community-modules/styles/src/internal/themes/alpine/_alpine-variables.scss index f48e3ede7f2..da4ab07bc76 100644 --- a/community-modules/styles/src/internal/themes/alpine/_alpine-variables.scss +++ b/community-modules/styles/src/internal/themes/alpine/_alpine-variables.scss @@ -138,6 +138,21 @@ --ag-cell-batch-edit-text-color: #f3d0b3; + --ag-formula-token-1-color: #4da3e5; + --ag-formula-token-1-background-color: rgb(77 163 229 / 16%); + --ag-formula-token-2-color: #f55864; + --ag-formula-token-2-background-color: rgb(245 88 100 / 16%); + --ag-formula-token-3-color: #b688f2; + --ag-formula-token-3-background-color: rgb(182 136 242 / 16%); + --ag-formula-token-4-color: #24bb4a; + --ag-formula-token-4-background-color: rgb(36 187 74 / 16%); + --ag-formula-token-5-color: #e772ba; + --ag-formula-token-5-background-color: rgb(231 114 186 / 16%); + --ag-formula-token-6-color: #f69b5f; + --ag-formula-token-6-background-color: rgb(246 155 95 / 16%); + --ag-formula-token-7-color: #a3e6ff; + --ag-formula-token-7-background-color: rgb(163 230 255 / 16%); + color-scheme: dark; } diff --git a/community-modules/styles/src/internal/themes/balham/_balham-variables.scss b/community-modules/styles/src/internal/themes/balham/_balham-variables.scss index 77f63d0d4b9..0249a1c950e 100644 --- a/community-modules/styles/src/internal/themes/balham/_balham-variables.scss +++ b/community-modules/styles/src/internal/themes/balham/_balham-variables.scss @@ -135,6 +135,21 @@ --ag-cell-batch-edit-text-color: #f3d0b3; + --ag-formula-token-1-color: #4da3e5; + --ag-formula-token-1-background-color: rgb(77 163 229 / 16%); + --ag-formula-token-2-color: #f55864; + --ag-formula-token-2-background-color: rgb(245 88 100 / 16%); + --ag-formula-token-3-color: #b688f2; + --ag-formula-token-3-background-color: rgb(182 136 242 / 16%); + --ag-formula-token-4-color: #24bb4a; + --ag-formula-token-4-background-color: rgb(36 187 74 / 16%); + --ag-formula-token-5-color: #e772ba; + --ag-formula-token-5-background-color: rgb(231 114 186 / 16%); + --ag-formula-token-6-color: #f69b5f; + --ag-formula-token-6-background-color: rgb(246 155 95 / 16%); + --ag-formula-token-7-color: #a3e6ff; + --ag-formula-token-7-background-color: rgb(163 230 255 / 16%); + color-scheme: dark; } diff --git a/community-modules/styles/src/internal/themes/material/_material-variables.scss b/community-modules/styles/src/internal/themes/material/_material-variables.scss index f3c12a0b23f..0f2d5ef6af6 100644 --- a/community-modules/styles/src/internal/themes/material/_material-variables.scss +++ b/community-modules/styles/src/internal/themes/material/_material-variables.scss @@ -140,6 +140,21 @@ --ag-cell-batch-edit-text-color: #f3d0b3; + --ag-formula-token-1-color: #4da3e5; + --ag-formula-token-1-background-color: rgb(77 163 229 / 16%); + --ag-formula-token-2-color: #f55864; + --ag-formula-token-2-background-color: rgb(245 88 100 / 16%); + --ag-formula-token-3-color: #b688f2; + --ag-formula-token-3-background-color: rgb(182 136 242 / 16%); + --ag-formula-token-4-color: #24bb4a; + --ag-formula-token-4-background-color: rgb(36 187 74 / 16%); + --ag-formula-token-5-color: #e772ba; + --ag-formula-token-5-background-color: rgb(231 114 186 / 16%); + --ag-formula-token-6-color: #f69b5f; + --ag-formula-token-6-background-color: rgb(246 155 95 / 16%); + --ag-formula-token-7-color: #a3e6ff; + --ag-formula-token-7-background-color: rgb(163 230 255 / 16%); + color-scheme: dark; } diff --git a/community-modules/styles/src/internal/themes/quartz/_quartz-variables.scss b/community-modules/styles/src/internal/themes/quartz/_quartz-variables.scss index 19d5e533fd0..1923d2c7493 100644 --- a/community-modules/styles/src/internal/themes/quartz/_quartz-variables.scss +++ b/community-modules/styles/src/internal/themes/quartz/_quartz-variables.scss @@ -150,6 +150,21 @@ --ag-cell-batch-edit-text-color: #f3d0b3; + --ag-formula-token-1-color: #4da3e5; + --ag-formula-token-1-background-color: rgb(77 163 229 / 16%); + --ag-formula-token-2-color: #f55864; + --ag-formula-token-2-background-color: rgb(245 88 100 / 16%); + --ag-formula-token-3-color: #b688f2; + --ag-formula-token-3-background-color: rgb(182 136 242 / 16%); + --ag-formula-token-4-color: #24bb4a; + --ag-formula-token-4-background-color: rgb(36 187 74 / 16%); + --ag-formula-token-5-color: #e772ba; + --ag-formula-token-5-background-color: rgb(231 114 186 / 16%); + --ag-formula-token-6-color: #f69b5f; + --ag-formula-token-6-background-color: rgb(246 155 95 / 16%); + --ag-formula-token-7-color: #a3e6ff; + --ag-formula-token-7-background-color: rgb(163 230 255 / 16%); + color-scheme: dark; } diff --git a/packages/ag-grid-community/src/theming/parts/color-scheme/color-schemes.ts b/packages/ag-grid-community/src/theming/parts/color-scheme/color-schemes.ts index dc4adfba71b..5ff33708c13 100644 --- a/packages/ag-grid-community/src/theming/parts/color-scheme/color-schemes.ts +++ b/packages/ag-grid-community/src/theming/parts/color-scheme/color-schemes.ts @@ -64,6 +64,14 @@ const darkParams = () => checkboxUncheckedBorderColor: foregroundBackgroundMix(0.4), toggleButtonOffBackgroundColor: foregroundBackgroundMix(0.4), rowBatchEditBackgroundColor: foregroundBackgroundMix(0.1), + // dark colours for formula editor / formula ranges + formulaToken1Color: '#4da3e5', + formulaToken2Color: '#f55864', + formulaToken3Color: '#b688f2', + formulaToken4Color: '#24bb4a', + formulaToken5Color: '#e772ba', + formulaToken6Color: '#f69b5f', + formulaToken7Color: '#a3e6ff', }) as const; const makeColorSchemeDarkTreeShakeable = () => diff --git a/packages/ag-grid-enterprise/src/widgets/agFormulaInputField.css b/packages/ag-grid-enterprise/src/widgets/agFormulaInputField.css index fc2126e8e9e..d9ecb944766 100644 --- a/packages/ag-grid-enterprise/src/widgets/agFormulaInputField.css +++ b/packages/ag-grid-enterprise/src/widgets/agFormulaInputField.css @@ -27,7 +27,7 @@ } .ag-formula-token-color-7 { - color: var(--ag-formula-token-6-color); + color: var(--ag-formula-token-7-color); } .ag-formula-range-color-1 { From 795d2a5a4985c1c250235d97b092c3657d7de3a9 Mon Sep 17 00:00:00 2001 From: Tak Tran Date: Mon, 19 Jan 2026 10:28:32 +0000 Subject: [PATCH 06/12] AG-3390 - Update sitemap page generation to use the latest sitemap (#12901) * AG-3390 - Extract git data util functions * AG-3390 - Add contact form result pages to sitemap ignore * AG-3390 - Cache the sitemap generated for future builds to use * AG-3390 - Build the site twice to get the latest sitemap The sitemap page requires the `sitemap-0.xml` file to be generated, but this can't be generated until the build is complete, so this runs the build twice, with the 1st run caching the `sitemap-0.xml` file for subsequent builds * AG-3390 - Extract get sitemap xml logic into shared folder * AG-3390 - Use cached xml sitemap even if cache hash is different Better to use the cache than to use the live site. More relevant for dev than for builds, as builds will generate twice to get the correct sitemap --- documentation/ag-grid-docs/astro.config.mjs | 6 ++ documentation/ag-grid-docs/project.json | 18 ++++-- documentation/ag-grid-docs/src/constants.ts | 1 + .../ag-grid-docs/src/pages/debug/meta.json.ts | 9 ++- .../ag-grid-docs/src/pages/sitemap.astro | 11 +++- .../ag-grid-docs/src/utils/sitemap.ts | 7 +- .../plugins/agCacheSitemap.ts | 53 +++++++++++++++ .../scripts/buildWithSitemapCache.ts | 43 +++++++++++++ external/ag-website-shared/src/constants.ts | 3 + .../src/utils/getSitemapXml.ts | 64 +++++++++++++++++++ .../ag-website-shared/src/utils/gitUtils.ts | 11 ++++ 11 files changed, 214 insertions(+), 12 deletions(-) create mode 100644 external/ag-website-shared/plugins/agCacheSitemap.ts create mode 100644 external/ag-website-shared/scripts/buildWithSitemapCache.ts create mode 100644 external/ag-website-shared/src/utils/getSitemapXml.ts create mode 100644 external/ag-website-shared/src/utils/gitUtils.ts diff --git a/documentation/ag-grid-docs/astro.config.mjs b/documentation/ag-grid-docs/astro.config.mjs index e877743456f..1aeea288d05 100644 --- a/documentation/ag-grid-docs/astro.config.mjs +++ b/documentation/ag-grid-docs/astro.config.mjs @@ -8,7 +8,9 @@ import { loadEnv } from 'vite'; import mkcert from 'vite-plugin-mkcert'; import svgr from 'vite-plugin-svgr'; +import agCacheSitemap from '../../external/ag-website-shared/plugins/agCacheSitemap'; import agLinkChecker from '../../external/ag-website-shared/plugins/agLinkChecker'; +import { SITEMAP_CACHE_DIR } from '../../external/ag-website-shared/src/constants'; import buildTime from './plugins/agBuildTime'; import agHotModuleReload from './plugins/agHotModuleReload'; import agHtaccessGen from './plugins/agHtaccessGen'; @@ -203,5 +205,9 @@ export default defineConfig({ skip: CHECK_REDIRECTS !== 'true', }), agLinkChecker({ include: CHECK_LINKS === 'true' }), + + agCacheSitemap({ + cacheFolder: SITEMAP_CACHE_DIR, + }), ], }); diff --git a/documentation/ag-grid-docs/project.json b/documentation/ag-grid-docs/project.json index a4e3c997d7a..7ca22736d4e 100644 --- a/documentation/ag-grid-docs/project.json +++ b/documentation/ag-grid-docs/project.json @@ -19,18 +19,28 @@ "!{projectRoot}/vitest.config.ts", "{workspaceRoot}/external/ag-website-shared/**", "charts", + "{projectRoot}/.astro/cache/sitemap/**", { "env": "PUBLIC_PACKAGE_VERSION" } ], "cache": true, - "command": "astro build", + "command": "tsx ../../external/ag-website-shared/scripts/buildWithSitemapCache.ts", "options": { "cwd": "{projectRoot}", "silent": true }, "configurations": { - "staging": {}, - "archive": {}, - "production": {}, + "staging": { + "silent": true, + "clean-cache": true + }, + "archive": { + "silent": true, + "clean-cache": true + }, + "production": { + "silent": true, + "clean-cache": true + }, "verbose": { "silent": null } diff --git a/documentation/ag-grid-docs/src/constants.ts b/documentation/ag-grid-docs/src/constants.ts index 245229f5b26..386e4e2d8df 100644 --- a/documentation/ag-grid-docs/src/constants.ts +++ b/documentation/ag-grid-docs/src/constants.ts @@ -91,6 +91,7 @@ export const SITE_URL = import.meta.env?.SITE_URL || import.meta.env?.PUBLIC_SIT export const STAGING_SITE_URL = 'https://grid-staging.ag-grid.com'; export const PRODUCTION_SITE_URLS = ['https://ag-grid.com', 'https://www.ag-grid.com']; +export const PRODUCTION_SITE_URL = PRODUCTION_SITE_URLS[0]; export const USE_PUBLISHED_PACKAGES = isTruthy(import.meta.env?.PUBLIC_USE_PUBLISHED_PACKAGES); export const URL_CONFIG: Record<'local' | 'staging' | 'production', { hosts: string[]; baseUrl?: string }> = { diff --git a/documentation/ag-grid-docs/src/pages/debug/meta.json.ts b/documentation/ag-grid-docs/src/pages/debug/meta.json.ts index 2d93326ba32..e33393a9e80 100644 --- a/documentation/ag-grid-docs/src/pages/debug/meta.json.ts +++ b/documentation/ag-grid-docs/src/pages/debug/meta.json.ts @@ -1,13 +1,12 @@ +import { getGitDate, getGitHash, getGitShortHash } from '@ag-website-shared/utils/gitUtils'; import { SITE_BASE_URL, SITE_URL, agChartsVersion, agGridVersion } from '@constants'; import { getIsArchive, getIsDev, getIsProduction, getIsStaging } from '@utils/env'; -import { execSync } from 'child_process'; export async function GET() { - const removeNewlineRegex = /\n/gm; const buildDate = new Date(); - const hash = execSync('git rev-parse HEAD').toString().replace(removeNewlineRegex, ''); - const shortHash = execSync('git rev-parse --short HEAD').toString().replace(removeNewlineRegex, ''); - const gitDate = execSync('git --no-pager log -1 --format="%ai"').toString().replace(removeNewlineRegex, ''); + const hash = getGitHash(); + const shortHash = getGitShortHash(); + const gitDate = getGitDate(); const body = { buildDate, diff --git a/documentation/ag-grid-docs/src/pages/sitemap.astro b/documentation/ag-grid-docs/src/pages/sitemap.astro index d3861fa53a5..ae97c2fe30a 100644 --- a/documentation/ag-grid-docs/src/pages/sitemap.astro +++ b/documentation/ag-grid-docs/src/pages/sitemap.astro @@ -1,10 +1,17 @@ --- import { Sitemap } from '@ag-website-shared/components/sitemap/Sitemap'; import parseSitemap from '@ag-website-shared/components/sitemap/utils/sitemaputils'; +import { getSitemapXml } from '@ag-website-shared/utils/getSitemapXml'; import Layout from '../layouts/Layout.astro'; +import { PRODUCTION_SITE_URL } from '@constants'; +import { SITEMAP_CACHE_DIR } from '@ag-website-shared/constants'; + +const sitemapUrl = `${PRODUCTION_SITE_URL.replace(/\/$/, '')}/sitemap-0.xml`; +const xmlSitemap = await getSitemapXml({ + cacheDir: SITEMAP_CACHE_DIR, + sitemapUrl, +}); -const response = await fetch('https://www.ag-grid.com/sitemap-0.xml'); -const xmlSitemap = await response.text(); const parsedSitemap = parseSitemap(xmlSitemap); --- diff --git a/documentation/ag-grid-docs/src/utils/sitemap.ts b/documentation/ag-grid-docs/src/utils/sitemap.ts index 9b342192447..a9675384170 100644 --- a/documentation/ag-grid-docs/src/utils/sitemap.ts +++ b/documentation/ag-grid-docs/src/utils/sitemap.ts @@ -46,7 +46,12 @@ const isRedirectPage = (page: string) => { * Exclude specific pages */ const isNonPublicContent = (page: string) => { - return page.endsWith('/style-guide/'); + return ( + page.endsWith('/style-guide/') || + // Contact form result pages + page.endsWith('/contact/failure/') || + page.endsWith('/contact/success/') + ); }; const filterIgnoredPages = (page: string) => { diff --git a/external/ag-website-shared/plugins/agCacheSitemap.ts b/external/ag-website-shared/plugins/agCacheSitemap.ts new file mode 100644 index 00000000000..0cdf9810c55 --- /dev/null +++ b/external/ag-website-shared/plugins/agCacheSitemap.ts @@ -0,0 +1,53 @@ +import type { AstroIntegration } from 'astro'; +import { promises as fs } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { getGitHash } from '../src/utils/gitUtils'; + +type Options = { + cacheFolder: string; +}; + +export default function createPlugin({ cacheFolder }: Options): AstroIntegration { + return { + name: 'ag-cache-sitemap', + hooks: { + 'astro:build:done': async ({ dir, logger }) => { + const currentHash = getGitHash(); + const outputDir = fileURLToPath(dir); + const sitemapSourcePath = path.join(outputDir, 'sitemap-0.xml'); + const metaSourcePath = path.join(outputDir, 'debug', 'meta.json'); + + const cacheRoot = path.resolve(cacheFolder); + const cacheSitemapPath = path.join(cacheRoot, 'sitemap-0.xml'); + const cacheMetaPath = path.join(cacheRoot, 'debug', 'meta.json'); + + const readCachedHash = async () => { + try { + const raw = await fs.readFile(cacheMetaPath, 'utf8'); + const cachedMeta = JSON.parse(raw); + return cachedMeta?.git?.hash ?? null; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return null; + } + + throw error; + } + }; + + const cachedHash = await readCachedHash(); + if (cachedHash === currentHash) { + logger.info(`Sitemap cache already up to date for ${currentHash}.`); + return; + } + + await fs.mkdir(path.dirname(cacheMetaPath), { recursive: true }); + await fs.copyFile(sitemapSourcePath, cacheSitemapPath); + await fs.copyFile(metaSourcePath, cacheMetaPath); + logger.info(`Cached sitemap and meta.json for ${currentHash}.`); + }, + }, + }; +} diff --git a/external/ag-website-shared/scripts/buildWithSitemapCache.ts b/external/ag-website-shared/scripts/buildWithSitemapCache.ts new file mode 100644 index 00000000000..c2c682dd099 --- /dev/null +++ b/external/ag-website-shared/scripts/buildWithSitemapCache.ts @@ -0,0 +1,43 @@ +#!/usr/bin/env tsx +import { spawnSync } from 'node:child_process'; +import { rmSync } from 'node:fs'; +import path from 'node:path'; + +import { SITEMAP_CACHE_DIR } from '../src/constants'; + +const rawArgs = process.argv.slice(2); +const hasFlag = (flag: string) => + rawArgs.includes(flag) || rawArgs.some((arg) => arg.startsWith(`${flag}=`) && arg !== `${flag}=false`); +const skipSecondBuild = hasFlag('--skip-second-build'); +const cleanCache = hasFlag('--clean-cache'); +const astroArgs = [ + 'build', + ...rawArgs.filter((arg) => !arg.startsWith('--skip-second-build') && !arg.startsWith('--clean-cache')), +]; + +const cleanSitemapCache = async () => { + const cacheFolder = path.resolve(SITEMAP_CACHE_DIR); + rmSync(cacheFolder, { recursive: true, force: true }); +}; + +const runBuild = () => { + const result = spawnSync('astro', astroArgs, { stdio: 'inherit', shell: true }); + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +}; + +if (cleanCache) { + console.log('✨ Cleaning sitemap cache'); + cleanSitemapCache(); +} + +runBuild(); + +if (!skipSecondBuild) { + if (!rawArgs.includes('--silent')) { + console.log('♻️ Building again to use latest sitemap'); + } + + runBuild(); +} diff --git a/external/ag-website-shared/src/constants.ts b/external/ag-website-shared/src/constants.ts index 38c066deb23..968d37c13cf 100644 --- a/external/ag-website-shared/src/constants.ts +++ b/external/ag-website-shared/src/constants.ts @@ -23,3 +23,6 @@ export const CONTACT_FORM_DATA = { formLocationId: '00NQ500000CVgqT', }, }; + +// Relative to website folder +export const SITEMAP_CACHE_DIR = '.astro/cache/sitemap'; diff --git a/external/ag-website-shared/src/utils/getSitemapXml.ts b/external/ag-website-shared/src/utils/getSitemapXml.ts new file mode 100644 index 00000000000..b967073e1e7 --- /dev/null +++ b/external/ag-website-shared/src/utils/getSitemapXml.ts @@ -0,0 +1,64 @@ +import { promises as fs } from 'node:fs'; +import path from 'node:path'; + +import { getGitHash } from './gitUtils'; + +type Logger = Pick; + +type GetSitemapXmlOptions = { + cacheDir: string; + sitemapUrl: string; + logger?: Logger; + gitHash?: string; +}; + +const readCachedHash = async (cachedMetaPath: string) => { + try { + const raw = await fs.readFile(cachedMetaPath, 'utf8'); + const cachedMeta = JSON.parse(raw); + return cachedMeta?.git?.hash ?? null; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return null; + } + + throw error; + } +}; + +export const getSitemapXml = async ({ + cacheDir, + sitemapUrl, + logger = console, + gitHash, +}: GetSitemapXmlOptions): Promise => { + const cacheFolder = path.resolve(cacheDir); + const cachedSitemapPath = path.join(cacheFolder, 'sitemap-0.xml'); + const cachedMetaPath = path.join(cacheFolder, 'debug', 'meta.json'); + const currentHash = gitHash ?? getGitHash(); + + let xmlSitemap: string | null = null; + try { + await fs.access(cachedSitemapPath); + const cachedHash = await readCachedHash(cachedMetaPath); + + xmlSitemap = await fs.readFile(cachedSitemapPath, 'utf8'); + if (cachedHash === currentHash) { + logger.info(`✅ Sitemap cache hash match. Using '${cachedSitemapPath}' for hash '${currentHash}'`); + } else { + logger.warn(`⚠️ Sitemap cache hash mismatch. Current: ${currentHash}, cached: ${cachedHash ?? 'unknown'}.`); + } + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { + throw error; + } + } + + if (xmlSitemap == null) { + const response = await fetch(sitemapUrl); + xmlSitemap = await response.text(); + logger.log('⚠️ No cached sitemap found, fetched from live site.'); + } + + return xmlSitemap; +}; diff --git a/external/ag-website-shared/src/utils/gitUtils.ts b/external/ag-website-shared/src/utils/gitUtils.ts new file mode 100644 index 00000000000..48960b9bf28 --- /dev/null +++ b/external/ag-website-shared/src/utils/gitUtils.ts @@ -0,0 +1,11 @@ +import { execSync } from 'child_process'; + +const removeNewlineRegex = /\n/gm; + +export const getGitHash = () => execSync('git rev-parse HEAD').toString().replace(removeNewlineRegex, ''); + +export const getGitShortHash = () => + execSync('git rev-parse --short=8 HEAD').toString().replace(removeNewlineRegex, ''); + +export const getGitDate = () => + execSync('git --no-pager log -1 --format="%ai"').toString().replace(removeNewlineRegex, ''); From ae7024585a3754d5e02c26affb12b28f12c2dc9a Mon Sep 17 00:00:00 2001 From: Alan Treadway Date: Mon, 19 Jan 2026 10:50:23 +0000 Subject: [PATCH 07/12] Integrate sync-rulesync.sh. --- .github/workflows/ci.yml | 3 +++ .nxignore | 9 +++++++++ .prettierignore | 9 +++++++++ package.json | 2 +- patches/rulesync+5.2.0.patch | 1 + 5 files changed, 23 insertions(+), 1 deletion(-) create mode 120000 patches/rulesync+5.2.0.patch diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 031ed658cb9..e3cc50f4470 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -302,6 +302,9 @@ jobs: with: fetch-depth: 1 # shallow copy + - name: Verify rulesync configuration + run: ./external/ag-shared/scripts/sync-rulesync/sync-rulesync.sh --check + - name: Setup id: setup uses: ./.github/actions/setup-nx diff --git a/.nxignore b/.nxignore index 01a829083e6..622500af342 100644 --- a/.nxignore +++ b/.nxignore @@ -1,2 +1,11 @@ # Needed to avoid nx format failing on symlinks. .rulesync/ +.patches/ +patches/ + +# Files without Prettier parsers +*.sh +*.patch +.gitrepo +.nxignore +.prettierignore diff --git a/.prettierignore b/.prettierignore index 96898e00997..5afe60891bb 100644 --- a/.prettierignore +++ b/.prettierignore @@ -41,3 +41,12 @@ testing/ag-grid-docs/test-results/ testing/angular-package-tests/project.base.json documentation/ag-grid-docs/src/components/ZoomInfo.astro + +# Files without Prettier parsers +*.patch +*.sh +.gitrepo +.nxignore + +# External subrepo (managed separately) +external/ag-shared/ diff --git a/package.json b/package.json index 8d9ca5b520f..c266a2bacac 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "compressVideo": "tsx external/ag-website-shared/scripts/compress-video", "build-generators": "nx run-many -p ag-grid-task-autogen -t build", "subrepo": "tsx external/ag-shared/scripts/subrepo", - "postinstall": "patch-package && yarn run build-generators && yarn setup-prompts", + "postinstall": "./external/ag-shared/scripts/apply-patches.sh && yarn run build-generators && yarn setup-prompts", "setup-prompts": "./external/ag-shared/scripts/setup-prompts/setup-prompts.sh", "bootstrap": "SHARP_IGNORE_GLOBAL_LIBVIPS=true YARN_REGISTRY=\"https://registry.ag-grid.com\" yarn", "git-clean": "git clean -fdx && git reset --hard HEAD", diff --git a/patches/rulesync+5.2.0.patch b/patches/rulesync+5.2.0.patch new file mode 120000 index 00000000000..5f7092731af --- /dev/null +++ b/patches/rulesync+5.2.0.patch @@ -0,0 +1 @@ +../external/ag-shared/prompts/patches/rulesync+5.2.0.patch \ No newline at end of file From 9d92e2859b4d66430b000d646ee36cdba1aa3116 Mon Sep 17 00:00:00 2001 From: Bernie Sumption Date: Mon, 19 Jan 2026 11:03:32 +0000 Subject: [PATCH 08/12] AG-16287 theming api selector performance improvements (#12900) * AG-16287 Add lint rule for low performance selectors * AG-16287 Fix Theming API CSS to avoid low-performance selectors * AG-16287 get lint plugin tests to run in `nx test` * AG-16287 fix lint errors --- .stylelintrc.js | 16 +- eslint.config.mjs | 2 +- package.json | 1 + .../src/agStack/core/agComponentStub.ts | 28 ++- .../agStack/theming/shared/css/_general.css | 18 +- .../src/agStack/theming/shared/css/_icons.css | 17 +- .../src/agStack/theming/shared/css/_popup.css | 7 +- .../src/agStack/theming/shared/css/_reset.css | 43 ++-- .../src/agStack/widgets/agAbstractLabel.css | 12 +- .../widgets/agContentEditableField.css | 9 +- .../src/agStack/widgets/agListItem.ts | 1 + .../src/agStack/widgets/agPickerField.css | 26 +-- .../src/agStack/widgets/agSelect.css | 18 +- .../src/agStack/widgets/agToggleButton.css | 50 ++--- .../src/edit/cell-editing.css | 2 +- .../src/filter/column-filters.css | 25 ++- .../src/filter/filterComp.ts | 2 + .../src/filter/legacyFilter.css | 11 + .../src/selection/rowSelection.css | 12 +- .../src/theming/core/css/_general.css | 25 ++- .../src/theming/core/css/_grid-layout.css | 8 +- .../src/theming/core/css/_header.css | 83 ++++---- .../src/theming/core/css/_icon-buttons.css | 51 +++-- .../parts/button-style/button-style-base.css | 12 +- .../checkbox-style/checkbox-style-default.css | 63 +++--- .../parts/input-style/input-style-base.css | 14 +- .../parts/tab-style/tab-style-base.css | 58 ++--- .../src/advancedFilter/advanced-filter.css | 48 +++-- .../autocomplete/agAutocomplete.css | 8 +- .../advancedFilterBuilderItemAddComp.ts | 2 +- .../builder/advancedFilterBuilderItemComp.ts | 6 +- .../src/charts/css/_charts.css | 96 ++++----- .../src/columnToolPanel/agPrimaryCols.css | 7 +- .../src/columnToolPanel/columnToolPanel.css | 6 +- .../src/filterToolPanel/filtersToolPanel.css | 5 +- .../newFiltersToolPanel.css | 19 +- .../src/license/watermark.css | 18 +- .../src/sideBar/agSideBar.css | 52 +++-- .../src/widgets/agRichSelect.css | 8 +- .../src/widgets/pillDropZonePanel.css | 16 +- .../LOW_PERFORMANCE_SELECTORS.md | 166 +++++++++++++++ plugins/stylelint-plugin-ag/index.mjs | 3 + plugins/stylelint-plugin-ag/jest.config.js | 6 + plugins/stylelint-plugin-ag/project.json | 26 +++ .../rules/no-low-performance-key-selector.mjs | 199 ++++++++++++++++++ .../no-low-performance-key-selector.test.mjs | 184 ++++++++++++++++ utilities/all/project.json | 3 +- 47 files changed, 1069 insertions(+), 423 deletions(-) create mode 100644 packages/ag-grid-community/src/filter/legacyFilter.css create mode 100644 plugins/stylelint-plugin-ag/LOW_PERFORMANCE_SELECTORS.md create mode 100644 plugins/stylelint-plugin-ag/index.mjs create mode 100644 plugins/stylelint-plugin-ag/jest.config.js create mode 100644 plugins/stylelint-plugin-ag/project.json create mode 100644 plugins/stylelint-plugin-ag/rules/no-low-performance-key-selector.mjs create mode 100644 plugins/stylelint-plugin-ag/rules/no-low-performance-key-selector.test.mjs diff --git a/.stylelintrc.js b/.stylelintrc.js index dd97cfb10b2..7146f503efb 100644 --- a/.stylelintrc.js +++ b/.stylelintrc.js @@ -1,6 +1,8 @@ module.exports = { extends: 'stylelint-config-standard', + plugins: ['./plugins/stylelint-plugin-ag/index.mjs'], rules: { + 'ag/no-low-performance-key-selector': true, 'comment-empty-line-before': [ 'always', { @@ -48,19 +50,5 @@ module.exports = { ], }, ], - - // NOTE: In general we want to avoid targeting grid elements using - // [class^='ag-'] as customer applications can have elements with that - // prefix too. Sometimes it is unavoidable, e.g. for global style - // resets, in which case scope the selector so it is only applied within - // the grid root. - 'selector-disallowed-list': [ - ['/.*class\\^=.*/'], - { - message: - 'Avoid selectors that target partial classnames unless absolutely necessary - see note in .stylelintrc.js', - severity: 'error', - }, - ], }, }; diff --git a/eslint.config.mjs b/eslint.config.mjs index 8a2dd20adf4..e337149fe5d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -112,7 +112,7 @@ export default [ // cypress uses a global API based on undefined variables files: [ '**/*.spec.{ts,js}', - '**/*test.{ts,js}', + '**/*test.{ts,js,mjs}', '**/{cypress,_copiedFromCore,__tests__}/**', '**/test-utils/**', ], diff --git a/package.json b/package.json index 821e6df9e93..12c963c4ec7 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "patch-package": "^8.0.0", "postcss-import": "^16.1.0", "postcss-rtlcss": "^5.3.1", + "postcss-selector-parser": "^7.1.0", "postcss-url": "^10.1.3", "prettier": "3.2.5", "prompts": "^2.4.2", diff --git a/packages/ag-grid-community/src/agStack/core/agComponentStub.ts b/packages/ag-grid-community/src/agStack/core/agComponentStub.ts index ec4c0d4301f..4ae0dbc3275 100644 --- a/packages/ag-grid-community/src/agStack/core/agComponentStub.ts +++ b/packages/ag-grid-community/src/agStack/core/agComponentStub.ts @@ -56,7 +56,7 @@ export class AgComponentStub< // if false, then CSS class "ag-invisible" is applied, which sets "visibility: hidden" private visible = true; - private css: string[] | undefined; + private css: string[] | undefined | typeof globalCssAdded; protected parentComponent: AgComponent | undefined; @@ -83,10 +83,7 @@ export class AgComponentStub< public preConstruct(): void { this.wireTemplate(this.getGui()); - const debugId = 'component-' + Object.getPrototypeOf(this)?.constructor?.name; - for (const css of this.css ?? []) { - this.beans.environment.addGlobalCSS(css, debugId); - } + this.addGlobalCss(); } private wireTemplate(element: HTMLElement | undefined, paramsMap?: { [key: string]: any }): void { @@ -413,7 +410,24 @@ export class AgComponentStub< } protected registerCSS(css: string): void { - this.css ||= []; - this.css.push(css); + if (this.css === globalCssAdded) { + this.css = [css]; + this.addGlobalCss(); + } else { + this.css ||= []; + this.css.push(css); + } + } + + private addGlobalCss(): void { + if (Array.isArray(this.css)) { + const debugId = 'component-' + Object.getPrototypeOf(this)?.constructor?.name; + for (const css of this.css ?? []) { + this.beans.environment.addGlobalCSS(css, debugId); + } + } + this.css = globalCssAdded; } } + +const globalCssAdded: unique symbol = Symbol(); diff --git a/packages/ag-grid-community/src/agStack/theming/shared/css/_general.css b/packages/ag-grid-community/src/agStack/theming/shared/css/_general.css index 657412cc737..e9ea5c40ff7 100644 --- a/packages/ag-grid-community/src/agStack/theming/shared/css/_general.css +++ b/packages/ag-grid-community/src/agStack/theming/shared/css/_general.css @@ -30,14 +30,14 @@ .ag-measurement-element-border { display: inline-block; +} - /* ResizeObserver measures the content box of an element so i order to get - it to pick up a border width, the border needs to be on a child of the - mesasurement element */ - &::before { - content: ''; - display: block; - /* rtl:ignore */ - border-left: var(--ag-internal-measurement-border); - } +/* ResizeObserver measures the content box of an element so in order to get + it to pick up a border width, the border needs to be on a child of the + measurement element */ +.ag-measurement-element-border::before { + content: ''; + display: block; + /* rtl:ignore */ + border-left: var(--ag-internal-measurement-border); } diff --git a/packages/ag-grid-community/src/agStack/theming/shared/css/_icons.css b/packages/ag-grid-community/src/agStack/theming/shared/css/_icons.css index 261b374ae43..7473cb740aa 100644 --- a/packages/ag-grid-community/src/agStack/theming/shared/css/_icons.css +++ b/packages/ag-grid-community/src/agStack/theming/shared/css/_icons.css @@ -32,13 +32,14 @@ background-repeat: no-repeat; } -.ag-disabled, -[disabled] { - .ag-icon { - opacity: 0.5; - } +.ag-disabled .ag-icon, +/* false positive bug, [disabled] is in ignore list */ +/* stylelint-disable-next-line selector-max-specificity */ +[disabled] .ag-icon { + opacity: 0.5; +} - &.ag-icon-grip { - opacity: 0.35; - } +.ag-icon-grip.ag-disabled, +.ag-icon-grip[disabled] { + opacity: 0.35; } diff --git a/packages/ag-grid-community/src/agStack/theming/shared/css/_popup.css b/packages/ag-grid-community/src/agStack/theming/shared/css/_popup.css index 40b328348a1..102efcf598a 100644 --- a/packages/ag-grid-community/src/agStack/theming/shared/css/_popup.css +++ b/packages/ag-grid-community/src/agStack/theming/shared/css/_popup.css @@ -1,9 +1,8 @@ .ag-popup-child { z-index: 5; top: 0; +} - /* use :where to avoid increasing specificity */ - &:where(:not(.ag-tooltip-custom)) { - box-shadow: var(--ag-popup-shadow); - } +.ag-popup-child:where(:not(.ag-tooltip-custom)) { + box-shadow: var(--ag-popup-shadow); } diff --git a/packages/ag-grid-community/src/agStack/theming/shared/css/_reset.css b/packages/ag-grid-community/src/agStack/theming/shared/css/_reset.css index 197fe0e5d29..219b78e7e47 100644 --- a/packages/ag-grid-community/src/agStack/theming/shared/css/_reset.css +++ b/packages/ag-grid-community/src/agStack/theming/shared/css/_reset.css @@ -1,38 +1,35 @@ /* NOTE: this list of root selectors is present in _root.css too, if you update one then don't forget the other */ -/* stylelint-disable-next-line selector-disallowed-list */ -:where(.ag-root-wrapper, .ag-external, .ag-popup, .ag-dnd-ghost, .ag-chart), -:where(.ag-root-wrapper, .ag-external, .ag-popup, .ag-dnd-ghost, .ag-chart) :where([class^='ag-']) { +/* stylelint-disable-next-line ag/no-low-performance-key-selector */ +:where([class^='ag-']), +:where([class^='ag-'])::after, +:where([class^='ag-'])::before { box-sizing: border-box; +} - &::after, - &::before { - box-sizing: border-box; - } - - &:where(div, span, label):focus-visible { - /* elements that are not normally focussable, but are made - focussable by the grid, get an inset shadow since they are often - right up against the edge of an overflow:hidden container that - will crop a regular shadow */ - outline: none; - box-shadow: inset var(--ag-focus-shadow); +/* stylelint-disable-next-line ag/no-low-performance-key-selector */ +:where([class^='ag-']):where(button) { + color: inherit; +} - &:where(.invalid) { - box-shadow: inset var(--ag-focus-error-shadow); - } - } +:where([class^='ag-']):where(div, span, label):focus-visible { + /* elements that are not normally focussable, but are made + focussable by the grid, get an inset shadow since they are often + right up against the edge of an overflow:hidden container that + will crop a regular shadow */ + outline: none; + box-shadow: inset var(--ag-focus-shadow); - &:where(button) { - color: inherit; + &:where(.invalid) { + box-shadow: inset var(--ag-focus-error-shadow); } } /* This next block must be on its own because browsers that don't support ::-ms-clear will ignore the whole block if it is placed in the above block */ -/* stylelint-disable-next-line selector-disallowed-list */ -:where(.ag-root-wrapper, ag-external, .ag-popup, .ag-dnd-ghost, .ag-chart) :where([class^='ag-']) ::-ms-clear { +/* stylelint-disable-next-line ag/no-low-performance-key-selector */ +:where([class^='ag-']) ::-ms-clear { display: none; } diff --git a/packages/ag-grid-community/src/agStack/widgets/agAbstractLabel.css b/packages/ag-grid-community/src/agStack/widgets/agAbstractLabel.css index 150d5e4890b..b26d29a0fa9 100644 --- a/packages/ag-grid-community/src/agStack/widgets/agAbstractLabel.css +++ b/packages/ag-grid-community/src/agStack/widgets/agAbstractLabel.css @@ -8,15 +8,21 @@ margin-left: var(--ag-spacing); } -.ag-label-align-right > * { - flex: none; +:where(.ag-label-align-right) { + .ag-label, + .ag-wrapper { + flex: none; + } } .ag-label-align-top { flex-direction: column; align-items: flex-start; +} - > * { +:where(.ag-label-align-top) { + .ag-label, + .ag-wrapper { align-self: stretch; } } diff --git a/packages/ag-grid-community/src/agStack/widgets/agContentEditableField.css b/packages/ag-grid-community/src/agStack/widgets/agContentEditableField.css index e7db02f21d7..47417b87469 100644 --- a/packages/ag-grid-community/src/agStack/widgets/agContentEditableField.css +++ b/packages/ag-grid-community/src/agStack/widgets/agContentEditableField.css @@ -17,15 +17,14 @@ line-height: normal; overflow: auto; overflow-y: hidden; - - &::-webkit-scrollbar { - display: none !important; - } - -ms-overflow-style: none !important; scrollbar-width: none !important; } +.ag-content-editable-field-input::-webkit-scrollbar { + display: none !important; +} + /* stylelint-disable-next-line selector-max-specificity */ .ag-wrapper.ag-content-editable-field-input { line-height: var(--ag-internal-content-line-height); diff --git a/packages/ag-grid-community/src/agStack/widgets/agListItem.ts b/packages/ag-grid-community/src/agStack/widgets/agListItem.ts index 626a12fb0c0..6193aaa8ac8 100644 --- a/packages/ag-grid-community/src/agStack/widgets/agListItem.ts +++ b/packages/ag-grid-community/src/agStack/widgets/agListItem.ts @@ -24,6 +24,7 @@ const getAgListElement = ( children: [ { tag: 'span', + cls: `ag-list-item-text ag-${cssIdentifier}-list-item-text`, ref: 'eText', children: label, }, diff --git a/packages/ag-grid-community/src/agStack/widgets/agPickerField.css b/packages/ag-grid-community/src/agStack/widgets/agPickerField.css index 4ff75eb2a89..9ddc98d0c21 100644 --- a/packages/ag-grid-community/src/agStack/widgets/agPickerField.css +++ b/packages/ag-grid-community/src/agStack/widgets/agPickerField.css @@ -22,24 +22,24 @@ background-color: var(--ag-picker-button-background-color); min-height: max(var(--ag-list-item-height), calc(var(--ag-spacing) * 4)); - &:where(.ag-picker-has-focus), - &:where(:focus-within) { - box-shadow: var(--ag-focus-shadow); - border: var(--ag-picker-button-focus-border); - background-color: var(--ag-picker-button-focus-background-color); - - &:where(.invalid) { - box-shadow: var(--ag-focus-error-shadow); - } - } - &:where(.invalid) { background-color: var(--ag-input-invalid-background-color); border: var(--ag-input-invalid-border); color: var(--ag-input-invalid-text-color); } +} - &:disabled { - opacity: 0.5; +.ag-picker-field-wrapper:where(.ag-picker-has-focus), +.ag-picker-field-wrapper:where(:focus-within) { + box-shadow: var(--ag-focus-shadow); + border: var(--ag-picker-button-focus-border); + background-color: var(--ag-picker-button-focus-background-color); + + &:where(.invalid) { + box-shadow: var(--ag-focus-error-shadow); } } + +.ag-picker-field-wrapper:disabled { + opacity: 0.5; +} diff --git a/packages/ag-grid-community/src/agStack/widgets/agSelect.css b/packages/ag-grid-community/src/agStack/widgets/agSelect.css index d4aaa21b3e5..a651c2f6cd9 100644 --- a/packages/ag-grid-community/src/agStack/widgets/agSelect.css +++ b/packages/ag-grid-community/src/agStack/widgets/agSelect.css @@ -7,6 +7,10 @@ } } +.ag-select:where(:not(.ag-cell-editor, .ag-label-align-top)) { + min-height: var(--ag-list-item-height); +} + :where(.ag-select) { .ag-picker-field-wrapper { padding-left: var(--ag-spacing); @@ -18,10 +22,6 @@ box-shadow: none; } - &:not(.ag-cell-editor, .ag-label-align-top) { - min-height: var(--ag-list-item-height); - } - .ag-picker-field-display { white-space: nowrap; overflow: hidden; @@ -46,10 +46,10 @@ padding-left: var(--ag-spacing); user-select: none; cursor: default; +} - :where(span) { - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - } +.ag-select-list-item-text { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; } diff --git a/packages/ag-grid-community/src/agStack/widgets/agToggleButton.css b/packages/ag-grid-community/src/agStack/widgets/agToggleButton.css index ab3688dcf0e..7ce27d6d1e6 100644 --- a/packages/ag-grid-community/src/agStack/widgets/agToggleButton.css +++ b/packages/ag-grid-community/src/agStack/widgets/agToggleButton.css @@ -25,36 +25,36 @@ height: var(--ag-toggle-button-height); } - &::before { - content: ''; - display: block; - position: absolute; - top: var(--ag-toggle-button-switch-inset); - /* rtl:ignore */ - left: var(--ag-toggle-button-switch-inset); - width: calc(var(--ag-toggle-button-height) - var(--ag-toggle-button-switch-inset) * 2); - height: calc(var(--ag-toggle-button-height) - var(--ag-toggle-button-switch-inset) * 2); - border-radius: 100%; - /* rtl:ignore */ - transition: left 100ms; - background-color: var(--ag-toggle-button-switch-background-color); - pointer-events: none; - } - &.ag-checked { background-color: var(--ag-toggle-button-on-background-color); - - &::before { - /* rtl:ignore */ - left: calc(100% - var(--ag-toggle-button-height) + var(--ag-toggle-button-switch-inset)); - } - } - - &:focus-within { - box-shadow: var(--ag-focus-shadow); } &.ag-disabled { opacity: 0.5; } } + +.ag-toggle-button-input-wrapper::before { + content: ''; + display: block; + position: absolute; + top: var(--ag-toggle-button-switch-inset); + /* rtl:ignore */ + left: var(--ag-toggle-button-switch-inset); + width: calc(var(--ag-toggle-button-height) - var(--ag-toggle-button-switch-inset) * 2); + height: calc(var(--ag-toggle-button-height) - var(--ag-toggle-button-switch-inset) * 2); + border-radius: 100%; + /* rtl:ignore */ + transition: left 100ms; + background-color: var(--ag-toggle-button-switch-background-color); + pointer-events: none; +} + +.ag-toggle-button-input-wrapper.ag-checked::before { + /* rtl:ignore */ + left: calc(100% - var(--ag-toggle-button-height) + var(--ag-toggle-button-switch-inset)); +} + +.ag-toggle-button-input-wrapper:focus-within { + box-shadow: var(--ag-focus-shadow); +} diff --git a/packages/ag-grid-community/src/edit/cell-editing.css b/packages/ag-grid-community/src/edit/cell-editing.css index 948d4102e48..8e1afff3ba0 100644 --- a/packages/ag-grid-community/src/edit/cell-editing.css +++ b/packages/ag-grid-community/src/edit/cell-editing.css @@ -36,7 +36,7 @@ padding: var(--ag-cell-horizontal-padding); } -:where(.ag-rtl .ag-large-text-input) textarea { +:where(.ag-rtl .ag-large-text-input) .ag-text-area-input { resize: none; } diff --git a/packages/ag-grid-community/src/filter/column-filters.css b/packages/ag-grid-community/src/filter/column-filters.css index 6848a1231fc..c7e23478739 100644 --- a/packages/ag-grid-community/src/filter/column-filters.css +++ b/packages/ag-grid-community/src/filter/column-filters.css @@ -21,7 +21,9 @@ display: block; margin-right: var(--ag-widget-container-horizontal-padding); - > * { + :where(.ag-set-filter-group-closed-icon), + :where(.ag-set-filter-group-opened-icon), + :where(.ag-set-filter-group-indeterminate-icon) { cursor: pointer; } } @@ -33,8 +35,7 @@ /* Add a minimum width for the filter menu to avoid having zero width when the content tries to size itself to the menu */ -:where(.ag-menu:not(.ag-tabs) .ag-filter) .ag-filter-body-wrapper, -:where(.ag-menu:not(.ag-tabs) .ag-filter) > *:not(.ag-filter-wrapper) { +:where(.ag-menu:not(.ag-tabs) .ag-filter) .ag-filter-body-wrapper { min-width: 180px; } @@ -63,16 +64,17 @@ overflow: hidden; } -:where(.ag-floating-filter-full-body) > div { - flex: 1 1 auto; -} - .ag-floating-filter-input { align-items: center; display: flex; width: 100%; - > * { + /* most filters */ + > :where(.ag-input-field), + /* date filters */ + > :where(.ag-date-floating-filter-wrapper), + /* nested multi filters */ + > :where(.ag-floating-filter-input) { flex: 1 1 auto; } @@ -94,7 +96,7 @@ display: flex; } -.ag-set-floating-filter-input :where(input)[disabled] { +.ag-set-floating-filter-input :where(.ag-input-field-input)[disabled] { pointer-events: none; } @@ -197,6 +199,7 @@ .ag-simple-filter-body-wrapper { display: flex; flex-direction: column; + gap: var(--ag-widget-vertical-spacing); padding: var(--ag-widget-container-vertical-padding) var(--ag-widget-container-horizontal-padding); padding-bottom: calc(var(--ag-widget-container-vertical-padding) - var(--ag-widget-vertical-spacing)); overflow-y: auto; @@ -204,10 +207,6 @@ var(--ag-list-item-height) + var(--ag-widget-container-vertical-padding) + var(--ag-widget-vertical-spacing) ); - & > * { - margin-bottom: var(--ag-widget-vertical-spacing); - } - :where(.ag-resizer-wrapper) { margin: 0; } diff --git a/packages/ag-grid-community/src/filter/filterComp.ts b/packages/ag-grid-community/src/filter/filterComp.ts index 7aa14349e0d..4758823c2f8 100644 --- a/packages/ag-grid-community/src/filter/filterComp.ts +++ b/packages/ag-grid-community/src/filter/filterComp.ts @@ -11,6 +11,7 @@ import { Component } from '../widgets/component'; import type { FilterDisplayWrapper } from './columnFilterService'; import { FilterWrapperComp } from './filterWrapperComp'; import type { FilterRequestSource } from './iColumnFilter'; +import { legacyFilterCSS } from './legacyFilter.css-GENERATED'; const FilterElement: ElementParams = { tag: 'div', cls: 'ag-filter' }; @@ -91,6 +92,7 @@ export class FilterComp extends Component { this.comp = displayComp; filterGui = displayComp.getGui(); } else { + this.registerCSS(legacyFilterCSS); filterGui = comp.getGui(); if (!_exists(filterGui)) { diff --git a/packages/ag-grid-community/src/filter/legacyFilter.css b/packages/ag-grid-community/src/filter/legacyFilter.css new file mode 100644 index 00000000000..a9dd34cea4b --- /dev/null +++ b/packages/ag-grid-community/src/filter/legacyFilter.css @@ -0,0 +1,11 @@ +/* `> *` is required for legacy custom filters where custom component DOM is + added directly to .ag-filter without .ag-filter-wrapper. To prevent slowing + down applications that don't use legacy custom filters. We add this file + dynamically when legacy custom filters are first used. */ + +/* stylelint-disable ag/no-low-performance-key-selector */ +:where(.ag-menu:not(.ag-tabs) .ag-filter) > *:not(.ag-filter-wrapper) { + /* Add a minimum width for the filter menu to avoid having zero width when the + content tries to size itself to the menu */ + min-width: 180px; +} diff --git a/packages/ag-grid-community/src/selection/rowSelection.css b/packages/ag-grid-community/src/selection/rowSelection.css index 0410d63db84..0b4baa73d0c 100644 --- a/packages/ag-grid-community/src/selection/rowSelection.css +++ b/packages/ag-grid-community/src/selection/rowSelection.css @@ -1,8 +1,6 @@ -:where(.ag-selection-checkbox) .ag-checkbox-input-wrapper { - &::before { - content: ''; - position: absolute; - inset: -8px; - cursor: pointer; - } +:where(.ag-selection-checkbox) .ag-checkbox-input-wrapper::before { + content: ''; + position: absolute; + inset: -8px; + cursor: pointer; } diff --git a/packages/ag-grid-community/src/theming/core/css/_general.css b/packages/ag-grid-community/src/theming/core/css/_general.css index e6d18926a8b..1ec8c6f1825 100644 --- a/packages/ag-grid-community/src/theming/core/css/_general.css +++ b/packages/ag-grid-community/src/theming/core/css/_general.css @@ -79,15 +79,20 @@ .ag-sticky-top-viewport, .ag-sticky-bottom-viewport { overflow-x: auto; - - &::-webkit-scrollbar { - display: none !important; - } - -ms-overflow-style: none !important; scrollbar-width: none !important; } +.ag-body-viewport::-webkit-scrollbar, +.ag-center-cols-viewport::-webkit-scrollbar, +.ag-header-viewport::-webkit-scrollbar, +.ag-floating-top-viewport::-webkit-scrollbar, +.ag-floating-bottom-viewport::-webkit-scrollbar, +.ag-sticky-top-viewport::-webkit-scrollbar, +.ag-sticky-bottom-viewport::-webkit-scrollbar { + display: none !important; +} + .ag-body-viewport { display: flex; overflow-x: hidden; @@ -232,7 +237,8 @@ always be 0 and then making a row sticky causes the grid to scroll. */ transition: opacity 400ms; visibility: hidden; - &:where(.ag-scrollbar-scrolling, .ag-scrollbar-active) { + &:where(.ag-scrollbar-scrolling), + &:where(.ag-scrollbar-active) { visibility: visible; opacity: 1; } @@ -437,7 +443,9 @@ always be 0 and then making a row sticky causes the grid to scroll. */ .ag-sticky-bottom { box-sizing: content-box !important; - :where(.ag-pinned-left-sticky-bottom, .ag-sticky-bottom-container, .ag-pinned-right-sticky-bottom) { + :where(.ag-pinned-left-sticky-bottom), + :where(.ag-sticky-bottom-container), + :where(.ag-pinned-right-sticky-bottom) { border-top: var(--ag-row-border); } } @@ -474,7 +482,8 @@ always be 0 and then making a row sticky causes the grid to scroll. */ .ag-column-group-icons { display: block; - > * { + :where(.ag-column-group-closed-icon), + :where(.ag-column-group-opened-icon) { cursor: pointer; } } diff --git a/packages/ag-grid-community/src/theming/core/css/_grid-layout.css b/packages/ag-grid-community/src/theming/core/css/_grid-layout.css index e2b917c6587..c502b59d572 100644 --- a/packages/ag-grid-community/src/theming/core/css/_grid-layout.css +++ b/packages/ag-grid-community/src/theming/core/css/_grid-layout.css @@ -138,6 +138,9 @@ align-items: center; padding-left: calc(var(--ag-indentation-level) * var(--ag-row-group-indent-size)); + /* stylelint-disable-next-line ag/no-low-performance-key-selector -- + Requires a slow selector because we allow custom cell editor components + as direct children of the wrapper, so we can't control the class name */ > *:where(:not(.ag-cell-value, .ag-group-value)) { /* cell widgets like checkboxes don't respond to line-height, so set their height to vertically centre them */ @@ -285,8 +288,11 @@ background-image: linear-gradient(var(--ag-selected-row-background-color), var(--ag-selected-row-background-color)); } -/* Default the content position to relative so that it doesn't appear under the background */ +/* stylelint-disable-next-line ag/no-low-performance-key-selector -- Requires a + slow selector because the child might be a custom component so we can't + control the class name. */ .ag-row.ag-full-width-row.ag-row-group > * { + /* Default the content position to relative so that it doesn't appear under the background */ position: relative; } diff --git a/packages/ag-grid-community/src/theming/core/css/_header.css b/packages/ag-grid-community/src/theming/core/css/_header.css index 9dddf53a9f3..6da601e95b6 100644 --- a/packages/ag-grid-community/src/theming/core/css/_header.css +++ b/packages/ag-grid-community/src/theming/core/css/_header.css @@ -31,10 +31,8 @@ } .ag-header-row:where(:not(:first-child)) { - :where( - .ag-header-cell:not(.ag-header-span-height.ag-header-span-total, .ag-header-parent-hidden), - .ag-header-group-cell.ag-header-group-cell-with-group - ) { + :where(.ag-header-cell:not(.ag-header-span-height.ag-header-span-total, .ag-header-parent-hidden)), + :where(.ag-header-group-cell.ag-header-group-cell-with-group) { border-top: var(--ag-header-row-border); } } @@ -86,40 +84,46 @@ /* Implement header cell hover and moving background colors as a pseudo element, because some applications set the color of the header cells and we want the hover effect to overlay on top of this color rather than replacing it. */ -.ag-header-cell:where(:not(.ag-floating-filter)), -.ag-header-group-cell { - &::before { - content: ''; - position: absolute; - inset: 0; - /* Use two stacked background-images so that hover and moving colors can +.ag-header-cell:where(:not(.ag-floating-filter))::before, +.ag-header-group-cell::before { + content: ''; + position: absolute; + inset: 0; + + /* Use two stacked background-images so that hover and moving colors can be individually controlled and overlaid on top of each other. background-image can't be smoothly animated so instead define the colors as internal vars and transition them */ - background-image: linear-gradient(var(--ag-internal-hover-color), var(--ag-internal-hover-color)), - linear-gradient(var(--ag-internal-moving-color), var(--ag-internal-moving-color)); + background-image: linear-gradient(var(--ag-internal-hover-color), var(--ag-internal-hover-color)), + linear-gradient(var(--ag-internal-moving-color), var(--ag-internal-moving-color)); - --ag-internal-moving-color: transparent; - --ag-internal-hover-color: transparent; + --ag-internal-moving-color: transparent; + --ag-internal-hover-color: transparent; - transition: - --ag-internal-moving-color var(--ag-header-cell-background-transition-duration), - --ag-internal-hover-color var(--ag-header-cell-background-transition-duration); - } + transition: + --ag-internal-moving-color var(--ag-header-cell-background-transition-duration), + --ag-internal-hover-color var(--ag-header-cell-background-transition-duration); +} - &:where(:hover)::before { - --ag-internal-hover-color: var(--ag-header-cell-hover-background-color); - } +.ag-header-cell:where(:not(.ag-floating-filter)):where(:hover)::before, +.ag-header-group-cell:where(:hover)::before { + --ag-internal-hover-color: var(--ag-header-cell-hover-background-color); +} - &:where(.ag-header-cell-moving)::before { - --ag-internal-moving-color: var(--ag-header-cell-moving-background-color); - --ag-internal-hover-color: var(--ag-header-cell-hover-background-color); - } +.ag-header-cell:where(:not(.ag-floating-filter)):where(.ag-header-cell-moving)::before, +.ag-header-group-cell:where(.ag-header-cell-moving)::before { + --ag-internal-moving-color: var(--ag-header-cell-moving-background-color); + --ag-internal-hover-color: var(--ag-header-cell-hover-background-color); } -/* Ensure cell content shows above cell hover background */ -:where(.ag-header-cell:not(.ag-floating-filter) *, .ag-header-group-cell *) { +/* stylelint-disable-next-line ag/no-low-performance-key-selector -- Any child + of these components needs to be relative to appear above the hover + background. It seemed worth the slow selector in this case to make it more + maintainable and have low risk of regressions compared to maintaining a list + of children that can appear here */ +:where(.ag-header-cell:not(.ag-floating-filter) > *, .ag-header-group-cell > *) { + /* Ensure cell content shows above cell hover background */ position: relative; z-index: 1; } @@ -189,6 +193,9 @@ white-space: normal; } +/* stylelint-disable-next-line ag/no-low-performance-key-selector -- Requires a + slow selector because we allow custom header cell components as direct + children of the wrapper, so we can't control the class name */ .ag-header-cell-comp-wrapper-limited-height > * { overflow: hidden; } @@ -273,17 +280,17 @@ /* unpinned headers get their rezise handle on the right in normal mode and left in RTL mode */ right: -3px; +} - &::after { - content: ''; - position: absolute; - z-index: 1; - top: calc(50% - var(--ag-header-column-resize-handle-height) * 0.5); - left: calc(50% - var(--ag-header-column-resize-handle-width)); - width: var(--ag-header-column-resize-handle-width); - height: var(--ag-header-column-resize-handle-height); - background-color: var(--ag-header-column-resize-handle-color); - } +.ag-header-cell-resize::after { + content: ''; + position: absolute; + z-index: 1; + top: calc(50% - var(--ag-header-column-resize-handle-height) * 0.5); + left: calc(50% - var(--ag-header-column-resize-handle-width)); + width: var(--ag-header-column-resize-handle-width); + height: var(--ag-header-column-resize-handle-height); + background-color: var(--ag-header-column-resize-handle-color); } :where(.ag-header-cell.ag-header-span-height) .ag-header-cell-resize::after { diff --git a/packages/ag-grid-community/src/theming/core/css/_icon-buttons.css b/packages/ag-grid-community/src/theming/core/css/_icon-buttons.css index 70eae5c5e3f..32e3bf4c836 100644 --- a/packages/ag-grid-community/src/theming/core/css/_icon-buttons.css +++ b/packages/ag-grid-community/src/theming/core/css/_icon-buttons.css @@ -18,27 +18,46 @@ border-radius: var(--ag-icon-button-border-radius); background-color: var(--ag-icon-button-background-color); box-shadow: 0 0 0 var(--ag-icon-button-background-spread) var(--ag-icon-button-background-color); +} - &:hover { - background-color: var(--ag-icon-button-hover-background-color); - box-shadow: 0 0 0 var(--ag-icon-button-background-spread) var(--ag-icon-button-hover-background-color); - color: var(--ag-icon-button-hover-color); - } +.ag-header-cell-menu-button:hover, +.ag-header-cell-filter-button:hover, +.ag-panel-title-bar-button:hover, +.ag-header-expand-icon:hover, +.ag-column-group-icons:hover, +.ag-set-filter-group-icons:hover, +:where(.ag-group-expanded) .ag-icon:hover, +:where(.ag-group-contracted) .ag-icon:hover, +.ag-chart-settings-prev:hover, +.ag-chart-settings-next:hover, +.ag-group-title-bar-icon:hover, +.ag-column-select-header-icon:hover, +.ag-floating-filter-button-button:hover, +.ag-filter-toolpanel-expand:hover, +.ag-panel-title-bar-button-icon:hover, +.ag-chart-menu-icon:hover { + background-color: var(--ag-icon-button-hover-background-color); + box-shadow: 0 0 0 var(--ag-icon-button-background-spread) var(--ag-icon-button-hover-background-color); + color: var(--ag-icon-button-hover-color); } -:where(.ag-filter-active, .ag-filter-toolpanel-group-instance-header-icon, .ag-filter-toolpanel-instance-header-icon) { +:where(.ag-filter-active), +:where(.ag-filter-toolpanel-group-instance-header-icon), +:where(.ag-filter-toolpanel-instance-header-icon) { position: relative; +} - &::after { - content: ''; - position: absolute; - width: 6px; - height: 6px; - top: -1px; - right: -1px; - border-radius: 50%; - background-color: var(--ag-icon-button-active-indicator-color); - } +:where(.ag-filter-active)::after, +:where(.ag-filter-toolpanel-group-instance-header-icon)::after, +:where(.ag-filter-toolpanel-instance-header-icon)::after { + content: ''; + position: absolute; + width: 6px; + height: 6px; + top: -1px; + right: -1px; + border-radius: 50%; + background-color: var(--ag-icon-button-active-indicator-color); } .ag-filter-active { diff --git a/packages/ag-grid-community/src/theming/parts/button-style/button-style-base.css b/packages/ag-grid-community/src/theming/parts/button-style/button-style-base.css index 983c74dc599..32f046dcee0 100644 --- a/packages/ag-grid-community/src/theming/parts/button-style/button-style-base.css +++ b/packages/ag-grid-community/src/theming/parts/button-style/button-style-base.css @@ -35,12 +35,6 @@ padding: var(--ag-button-vertical-padding) var(--ag-button-horizontal-padding); cursor: pointer; - &:hover { - color: var(--ag-button-hover-text-color); - background-color: var(--ag-button-hover-background-color); - border: var(--ag-button-hover-border); - } - &:active { color: var(--ag-button-active-text-color); background-color: var(--ag-button-active-background-color); @@ -53,3 +47,9 @@ border: var(--ag-button-disabled-border); } } + +.ag-standard-button:hover { + color: var(--ag-button-hover-text-color); + background-color: var(--ag-button-hover-background-color); + border: var(--ag-button-hover-border); +} diff --git a/packages/ag-grid-community/src/theming/parts/checkbox-style/checkbox-style-default.css b/packages/ag-grid-community/src/theming/parts/checkbox-style/checkbox-style-default.css index a9d2a3407b4..39ebb814b83 100644 --- a/packages/ag-grid-community/src/theming/parts/checkbox-style/checkbox-style-default.css +++ b/packages/ag-grid-community/src/theming/parts/checkbox-style/checkbox-style-default.css @@ -7,37 +7,13 @@ background-color: var(--ag-checkbox-unchecked-background-color); border: solid var(--ag-checkbox-border-width) var(--ag-checkbox-unchecked-border-color); - :where(input) { - cursor: pointer; - appearance: none; - opacity: 0; - margin: 0; - display: block; - width: var(--ag-icon-size); - height: var(--ag-icon-size); - } - - &::after { - content: ''; - position: absolute; - display: block; - inset: 0; - mask-position: center; - mask-repeat: no-repeat; - pointer-events: none; - } - &:where(.ag-checked) { background-color: var(--ag-checkbox-checked-background-color); border-color: var(--ag-checkbox-checked-border-color); - - &::after { - background-color: var(--ag-checkbox-checked-shape-color); - } } - &:where(:focus-within, :active) { - box-shadow: var(--ag-focus-shadow); + &:where(.ag-checked)::after { + background-color: var(--ag-checkbox-checked-shape-color); } &:where(.ag-disabled) { @@ -46,6 +22,33 @@ } } +.ag-checkbox-input, +.ag-radio-button-input { + cursor: pointer; + appearance: none; + opacity: 0; + margin: 0; + display: block; + width: var(--ag-icon-size); + height: var(--ag-icon-size); +} + +.ag-checkbox-input-wrapper::after, +.ag-radio-button-input-wrapper::after { + content: ''; + position: absolute; + display: block; + inset: 0; + mask-position: center; + mask-repeat: no-repeat; + pointer-events: none; +} + +.ag-checkbox-input-wrapper:where(:focus-within, :active), +.ag-radio-button-input-wrapper:where(:focus-within, :active) { + box-shadow: var(--ag-focus-shadow); +} + .ag-checkbox-input-wrapper { border-radius: var(--ag-checkbox-border-radius); @@ -56,11 +59,11 @@ &:where(.ag-indeterminate) { background-color: var(--ag-checkbox-indeterminate-background-color); border-color: var(--ag-checkbox-indeterminate-border-color); + } - &::after { - background-color: var(--ag-checkbox-indeterminate-shape-color); - mask-image: var(--ag-checkbox-indeterminate-shape-image); - } + &:where(.ag-indeterminate)::after { + background-color: var(--ag-checkbox-indeterminate-shape-color); + mask-image: var(--ag-checkbox-indeterminate-shape-image); } } diff --git a/packages/ag-grid-community/src/theming/parts/input-style/input-style-base.css b/packages/ag-grid-community/src/theming/parts/input-style/input-style-base.css index b997a523327..c20396e902a 100644 --- a/packages/ag-grid-community/src/theming/parts/input-style/input-style-base.css +++ b/packages/ag-grid-community/src/theming/parts/input-style/input-style-base.css @@ -61,14 +61,12 @@ } /* icon for search inputs */ -:where( - .ag-column-select-header-filter-wrapper, - .ag-filter-toolpanel-search, - .ag-mini-filter, - .ag-filter-filter, - .ag-filter-add-select, - .ag-floating-filter-search-icon - ) { +:where(.ag-column-select-header-filter-wrapper), +:where(.ag-filter-toolpanel-search), +:where(.ag-mini-filter), +:where(.ag-filter-filter), +:where(.ag-filter-add-select), +:where(.ag-floating-filter-search-icon) { .ag-input-wrapper::before { position: absolute; display: block; diff --git a/packages/ag-grid-community/src/theming/parts/tab-style/tab-style-base.css b/packages/ag-grid-community/src/theming/parts/tab-style/tab-style-base.css index 0a890f97058..2e0f8ccdbcf 100644 --- a/packages/ag-grid-community/src/theming/parts/tab-style/tab-style-base.css +++ b/packages/ag-grid-community/src/theming/parts/tab-style/tab-style-base.css @@ -38,39 +38,39 @@ flex: 1; background-color: var(--ag-tab-background-color); color: var(--ag-tab-text-color); +} - &:hover { - background-color: var(--ag-tab-hover-background-color); - color: var(--ag-tab-hover-text-color); - } +.ag-tab:hover { + background-color: var(--ag-tab-hover-background-color); + color: var(--ag-tab-hover-text-color); +} - &.ag-tab-selected { - background-color: var(--ag-tab-selected-background-color); - color: var(--ag-tab-selected-text-color); +.ag-tab.ag-tab-selected { + background-color: var(--ag-tab-selected-background-color); + color: var(--ag-tab-selected-text-color); +} - &:where(:not(:first-of-type)) { - border-left-color: var(--ag-tab-selected-border-color); - } +.ag-tab.ag-tab-selected:where(:not(:first-of-type)) { + border-left-color: var(--ag-tab-selected-border-color); +} - &:where(:not(:last-of-type)) { - border-right-color: var(--ag-tab-selected-border-color); - } - } +.ag-tab.ag-tab-selected:where(:not(:last-of-type)) { + border-right-color: var(--ag-tab-selected-border-color); +} - &::after { - content: ''; - display: block; - position: absolute; - height: var(--ag-tab-selected-underline-width); - background-color: var(--ag-tab-selected-underline-color); - left: 0; - right: 0; - bottom: 0; - opacity: 0; - transition: opacity var(--ag-tab-selected-underline-transition-duration); - } +.ag-tab::after { + content: ''; + display: block; + position: absolute; + height: var(--ag-tab-selected-underline-width); + background-color: var(--ag-tab-selected-underline-color); + left: 0; + right: 0; + bottom: 0; + opacity: 0; + transition: opacity var(--ag-tab-selected-underline-transition-duration); +} - &.ag-tab-selected::after { - opacity: 1; - } +.ag-tab.ag-tab-selected::after { + opacity: 1; } diff --git a/packages/ag-grid-enterprise/src/advancedFilter/advanced-filter.css b/packages/ag-grid-enterprise/src/advancedFilter/advanced-filter.css index b04312066c0..99d40eaabff 100644 --- a/packages/ag-grid-enterprise/src/advancedFilter/advanced-filter.css +++ b/packages/ag-grid-enterprise/src/advancedFilter/advanced-filter.css @@ -35,10 +35,10 @@ font-weight: 600; padding: var(--ag-spacing); - &:where(:not(:disabled)) { + &:where(.ag-standard-button):where(:not(:disabled)) { cursor: pointer; - &:hover { + &:where(.ag-standard-button):hover { background-color: var(--ag-row-hover-color); } } @@ -89,17 +89,17 @@ position: absolute; } -.ag-advanced-filter-builder-item-tree-lines > * { +.ag-advanced-filter-builder-item-tree-line { width: var(--ag-advanced-filter-builder-indent-size); } .ag-advanced-filter-builder-item-tree-line-root { width: var(--ag-icon-size); +} - &::before { - top: 50%; - height: 50%; - } +.ag-advanced-filter-builder-item-tree-line-root::before { + top: 50%; + height: 50%; } .ag-advanced-filter-builder-item-tree-line-horizontal, @@ -110,13 +110,19 @@ height: 100%; display: flex; align-items: center; +} - &::before, - &::after { - content: ''; - position: absolute; - height: 100%; - } +.ag-advanced-filter-builder-item-tree-line-horizontal::before, +.ag-advanced-filter-builder-item-tree-line-horizontal::after, +.ag-advanced-filter-builder-item-tree-line-vertical::before, +.ag-advanced-filter-builder-item-tree-line-vertical::after, +.ag-advanced-filter-builder-item-tree-line-vertical-top::before, +.ag-advanced-filter-builder-item-tree-line-vertical-top::after, +.ag-advanced-filter-builder-item-tree-line-vertical-bottom::before, +.ag-advanced-filter-builder-item-tree-line-vertical-bottom::after { + content: ''; + position: absolute; + height: 100%; } .ag-advanced-filter-builder-item-tree-line-horizontal::after { @@ -225,8 +231,10 @@ } } -.ag-advanced-filter-builder-item-buttons > * { - margin: 0 calc(var(--ag-spacing) * 0.5); +.ag-advanced-filter-builder-item-buttons { + display: flex; + gap: var(--ag-spacing); + padding: 0 calc(var(--ag-spacing) * 0.5); } .ag-advanced-filter-builder-item-button { @@ -255,13 +263,13 @@ display: flex; cursor: default; height: var(--ag-list-item-height); +} - &:hover { - background-color: var(--ag-row-hover-color); +.ag-advanced-filter-builder-virtual-list-item:hover { + background-color: var(--ag-row-hover-color); - :where(.ag-advanced-filter-builder-item-button) { - opacity: 1; - } + :where(.ag-advanced-filter-builder-item-button) { + opacity: 1; } } diff --git a/packages/ag-grid-enterprise/src/advancedFilter/autocomplete/agAutocomplete.css b/packages/ag-grid-enterprise/src/advancedFilter/autocomplete/agAutocomplete.css index 3748af7aa30..4fc21d4b202 100644 --- a/packages/ag-grid-enterprise/src/advancedFilter/autocomplete/agAutocomplete.css +++ b/packages/ag-grid-enterprise/src/advancedFilter/autocomplete/agAutocomplete.css @@ -3,7 +3,7 @@ display: flex; width: 100%; - > * { + > :where(.ag-text-field) { flex: 1 1 auto; } } @@ -35,10 +35,10 @@ &:focus-visible::after { content: none; } +} - &:hover { - background-color: var(--ag-row-hover-color); - } +.ag-autocomplete-virtual-list-item:hover { + background-color: var(--ag-row-hover-color); } .ag-autocomplete-row { diff --git a/packages/ag-grid-enterprise/src/advancedFilter/builder/advancedFilterBuilderItemAddComp.ts b/packages/ag-grid-enterprise/src/advancedFilter/builder/advancedFilterBuilderItemAddComp.ts index 94cc68d2204..bcbcce10358 100644 --- a/packages/ag-grid-enterprise/src/advancedFilter/builder/advancedFilterBuilderItemAddComp.ts +++ b/packages/ag-grid-enterprise/src/advancedFilter/builder/advancedFilterBuilderItemAddComp.ts @@ -36,7 +36,7 @@ const ItemAddElement: ElementParams = { children: [ { tag: 'div', - cls: 'ag-advanced-filter-builder-item-tree-line-vertical-top ag-advanced-filter-builder-item-tree-line-horizontal', + cls: 'ag-advanced-filter-builder-item-tree-line ag-advanced-filter-builder-item-tree-line-vertical-top ag-advanced-filter-builder-item-tree-line-horizontal', }, ], }, diff --git a/packages/ag-grid-enterprise/src/advancedFilter/builder/advancedFilterBuilderItemComp.ts b/packages/ag-grid-enterprise/src/advancedFilter/builder/advancedFilterBuilderItemComp.ts index bc9d2d45b5d..272d9a2cda4 100644 --- a/packages/ag-grid-enterprise/src/advancedFilter/builder/advancedFilterBuilderItemComp.ts +++ b/packages/ag-grid-enterprise/src/advancedFilter/builder/advancedFilterBuilderItemComp.ts @@ -137,7 +137,7 @@ export class AdvancedFilterBuilderItemComp extends TabGuardComp * { - padding: 0; - margin: 0; - } - &.ag-charts-format-top-level-group, &.ag-charts-data-group { border-top: solid var(--ag-border-width) var(--ag-border-color); @@ -427,12 +419,20 @@ margin: 0; } - .ag-charts-format-top-level-group-container > *, - .ag-charts-format-sub-level-group-container > *, - .ag-charts-format-sub-level-no-header-group-container > * { + .ag-charts-format-top-level-group-item, + .ag-charts-format-sub-level-group-item, + .ag-charts-format-sub-level-no-header-group-item { margin-bottom: var(--ag-widget-vertical-spacing); } + &.ag-charts-format-top-level-group, + .ag-charts-format-top-level-group-item, + &.ag-charts-format-sub-level-group, + .ag-charts-format-sub-level-group-item:last-child { + padding: 0; + margin: 0; + } + .ag-charts-advanced-settings-top-level-group-container { margin: 0; } diff --git a/packages/ag-grid-enterprise/src/columnToolPanel/agPrimaryCols.css b/packages/ag-grid-enterprise/src/columnToolPanel/agPrimaryCols.css index 0f40b817300..961477fbaf1 100644 --- a/packages/ag-grid-enterprise/src/columnToolPanel/agPrimaryCols.css +++ b/packages/ag-grid-enterprise/src/columnToolPanel/agPrimaryCols.css @@ -28,10 +28,11 @@ height: 100%; gap: var(--ag-widget-horizontal-spacing); padding-left: calc(var(--ag-indentation-level) * var(--ag-column-select-indent-size)); +} - &:where(:not(:last-child)) { - margin-bottom: var(--ag-widget-vertical-spacing); - } +.ag-column-select-column:where(:not(:last-child)), +.ag-column-select-column-group:where(:not(:last-child)) { + margin-bottom: var(--ag-widget-vertical-spacing); } .ag-column-select-header-icon { diff --git a/packages/ag-grid-enterprise/src/columnToolPanel/columnToolPanel.css b/packages/ag-grid-enterprise/src/columnToolPanel/columnToolPanel.css index 7fb194d7174..c76975be55d 100644 --- a/packages/ag-grid-enterprise/src/columnToolPanel/columnToolPanel.css +++ b/packages/ag-grid-enterprise/src/columnToolPanel/columnToolPanel.css @@ -31,8 +31,8 @@ :where(.ag-column-panel) .ag-column-drop-vertical { min-height: 50px; flex: 1 1 0px; +} - &:where(:not(.ag-last-column-drop)) { - border-bottom: var(--ag-tool-panel-separator-border); - } +:where(.ag-column-panel) .ag-column-drop-vertical:where(:not(.ag-last-column-drop)) { + border-bottom: var(--ag-tool-panel-separator-border); } diff --git a/packages/ag-grid-enterprise/src/filterToolPanel/filtersToolPanel.css b/packages/ag-grid-enterprise/src/filterToolPanel/filtersToolPanel.css index ea17953f992..affa4cb0d4d 100644 --- a/packages/ag-grid-enterprise/src/filterToolPanel/filtersToolPanel.css +++ b/packages/ag-grid-enterprise/src/filterToolPanel/filtersToolPanel.css @@ -11,7 +11,10 @@ font-weight: var(--ag-header-font-weight); color: var(--ag-header-text-color); - > * { + > :where(.ag-filter-toolpanel-expand), + > :where(.ag-filter-toolpanel-search-input), + > :where(.ag-header-cell-text), + > :where(.ag-filter-icon) { display: flex; align-items: center; } diff --git a/packages/ag-grid-enterprise/src/filterToolPanel/newFilterToolPanel/newFiltersToolPanel.css b/packages/ag-grid-enterprise/src/filterToolPanel/newFilterToolPanel/newFiltersToolPanel.css index 452fd25a0cc..294313e1d48 100644 --- a/packages/ag-grid-enterprise/src/filterToolPanel/newFilterToolPanel/newFiltersToolPanel.css +++ b/packages/ag-grid-enterprise/src/filterToolPanel/newFilterToolPanel/newFiltersToolPanel.css @@ -28,10 +28,9 @@ flex: 1; overflow: auto; padding: var(--ag-widget-container-vertical-padding) var(--ag-widget-container-horizontal-padding) 0; -} - -.ag-filter-panel-container > *:where(:not(:last-child)) { - margin-bottom: var(--ag-widget-container-vertical-padding); + display: flex; + flex-direction: column; + gap: var(--ag-widget-container-vertical-padding); } .ag-filter-card { @@ -45,10 +44,7 @@ flex-direction: row; align-items: center; padding-top: var(--ag-widget-vertical-spacing); - - & > *:where(:not(:last-child)) { - padding-right: var(--ag-spacing); - } + gap: var(--ag-spacing); } .ag-filter-card-heading { @@ -184,13 +180,14 @@ background-color: var(--ag-filter-panel-apply-button-background-color); } -.ag-filter-panel > *:where(:last-child) { +.ag-filter-panel-container:where(:last-child), +.ag-filter-panel-buttons:where(:last-child) { padding-bottom: var(--ag-widget-container-vertical-padding); } /* stylelint-disable-next-line selector-max-specificity */ -.ag-filter-panel .ag-simple-filter-body-wrapper > *:last-child, +.ag-filter-panel .ag-simple-filter-body-wrapper, /* stylelint-disable-next-line selector-max-specificity */ .ag-filter-panel .ag-set-filter-body-wrapper { - margin-bottom: var(--ag-widget-container-vertical-padding); + padding-bottom: var(--ag-widget-container-vertical-padding); } diff --git a/packages/ag-grid-enterprise/src/license/watermark.css b/packages/ag-grid-enterprise/src/license/watermark.css index cfa09983cc2..4083b4b13ce 100644 --- a/packages/ag-grid-enterprise/src/license/watermark.css +++ b/packages/ag-grid-enterprise/src/license/watermark.css @@ -5,16 +5,16 @@ opacity: 0.7; transition: opacity 1s ease-out 3s; color: #9b9b9b; +} - &::before { - content: ''; - background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjA5IiBoZWlnaHQ9IjM2IiB2aWV3Qm94PSIwIDAgMjA5IDM2IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBkPSJNMTkyLjk5MyAyMy42NTgyVjE1LjcxMTdIMTc5LjQ1MkwxNzEuNTA1IDIzLjY1ODJIMTkyLjk5M1oiIGZpbGw9IiM5QjlCOUIiLz4KPHBhdGggZD0iTTIwOC4yNSAzLjk1MDgxSDE5MS4yNzZMMTgzLjI2NiAxMS44OTczSDIwOC4yNVYzLjk1MDgxWiIgZmlsbD0iIzlCOUI5QiIvPgo8cGF0aCBkPSJNMTYzLjYyMiAzMS42MDQ4TDE2Ny42OTEgMjcuNTM2MUgxODEuNDIzVjM1LjQ4MjdIMTYzLjYyMlYzMS42MDQ4WiIgZmlsbD0iIzlCOUI5QiIvPgo8cGF0aCBkPSJNMTY2LjYxIDE5Ljc4MDNIMTc1LjM4M0wxODMuMzkzIDExLjgzMzdIMTY2LjYxVjE5Ljc4MDNaIiBmaWxsPSIjOUI5QjlCIi8+CjxwYXRoIGQ9Ik0xNTcuMDExIDMxLjYwNDdIMTYzLjYyMkwxNzEuNTA1IDIzLjY1ODJIMTU3LjAxMVYzMS42MDQ3WiIgZmlsbD0iIzlCOUI5QiIvPgo8cGF0aCBkPSJNMTkxLjI3NiAzLjk1MDgxTDE4Ny4yMDggOC4wMTk0MUgxNjEuMjdWMC4wNzI4NzZIMTkxLjI3NlYzLjk1MDgxWiIgZmlsbD0iIzlCOUI5QiIvPgo8cGF0aCBkPSJNMjAuODM5MSAzMC4yMDYxSDguMzc4OTJMNi4yMTc0NSAzNS41NDYySDAuNzUwMjQ0TDEyLjI1NjggOC41OTE1NUgxNy4wMjQ3TDI4LjUzMTMgMzUuNTQ2MkgyMy4wMDA1TDIwLjgzOTEgMzAuMjA2MVpNMTkuMTIyNyAyNS45NDY4TDE0LjYwOSAxNC45NDg4TDEwLjA5NTQgMjUuOTQ2OEgxOS4xMjI3WiIgZmlsbD0iIzlCOUI5QiIvPgo8cGF0aCBkPSJNMTA0LjQzNyAxOC41MDg5QzEwNi4wMjYgMTYuMTU2NyAxMTAuMDMxIDE1LjkwMjQgMTExLjY4NCAxNS45MDI0VjIwLjQ3OTZDMTA5LjY1IDIwLjQ3OTYgMTA3LjYxNSAyMC41NDMyIDEwNi40MDcgMjEuNDMzMkMxMDUuMiAyMi4zMjMyIDEwNC41NjQgMjMuNTMxMSAxMDQuNTY0IDI0Ljk5MzJWMzUuNTQ2Mkg5OS42MDUxVjE1LjkwMjRIMTA0LjM3M0wxMDQuNDM3IDE4LjUwODlaIiBmaWxsPSIjOUI5QjlCIi8+CjxwYXRoIGQ9Ik0xMTkuMzc2IDE1LjkwMjRIMTE0LjQxOFYzNS41NDYySDExOS4zNzZWMTUuOTAyNFoiIGZpbGw9IiM5QjlCOUIiLz4KPHBhdGggZD0iTTExOS4zNzYgNy4xMjkzOUgxMTQuNDE4VjEyLjk3OEgxMTkuMzc2VjcuMTI5MzlaIiBmaWxsPSIjOUI5QjlCIi8+CjxwYXRoIGQ9Ik0xNDMuOTc5IDcuMTI5MzlWMzUuNTQ2MkgxMzkuMjExTDEzOS4wODQgMzIuNTU4M0MxMzguMzg0IDMzLjU3NTUgMTM3LjQ5NCAzNC40MDE5IDEzNi40MTQgMzUuMDM3NkMxMzUuMzMzIDM1LjYwOTggMTMzLjk5OCAzNS45Mjc2IDEzMi40NzIgMzUuOTI3NkMxMzEuMTM3IDM1LjkyNzYgMTI5Ljg2NiAzNS42NzMzIDEyOC43ODUgMzUuMjI4M0MxMjcuNjQxIDM0LjcxOTcgMTI2LjYyMyAzNC4wODQgMTI1Ljc5NyAzMy4xOTRDMTI0Ljk3MSAzMi4zMDQgMTI0LjI3MSAzMS4yMjMzIDEyMy44MjYgMzAuMDE1NEMxMjMuMzE4IDI4LjgwNzUgMTIzLjEyNyAyNy40MDkgMTIzLjEyNyAyNS44ODMyQzEyMy4xMjcgMjQuMzU3NSAxMjMuMzgxIDIyLjk1ODkgMTIzLjgyNiAyMS42ODc0QzEyNC4zMzUgMjAuNDE2IDEyNC45NzEgMTkuMzM1MyAxMjUuNzk3IDE4LjQ0NTNDMTI2LjYyMyAxNy41NTUyIDEyNy42NDEgMTYuODU2IDEyOC43ODUgMTYuMzQ3NEMxMjkuOTI5IDE1LjgzODggMTMxLjEzNyAxNS41ODQ1IDEzMi40NzIgMTUuNTg0NUMxMzMuOTk4IDE1LjU4NDUgMTM1LjI2OSAxNS44Mzg4IDEzNi4zNSAxNi40MTA5QzEzNy40MzEgMTYuOTgzMSAxMzguMzIxIDE3Ljc0NTkgMTM5LjAyIDE4LjgyNjdWNy4xOTI5NUgxNDMuOTc5VjcuMTI5MzlaTTEzMy41NTMgMzEuNjY4M0MxMzUuMjA2IDMxLjY2ODMgMTM2LjQ3NyAzMS4wOTYyIDEzNy40OTQgMzAuMDE1NEMxMzguNTExIDI4LjkzNDcgMTM5LjAyIDI3LjQ3MjUgMTM5LjAyIDI1LjY5MjVDMTM5LjAyIDIzLjkxMjUgMTM4LjUxMSAyMi41MTM5IDEzNy40OTQgMjEuMzY5NkMxMzYuNDc3IDIwLjI4ODggMTM1LjIwNiAxOS43MTY3IDEzMy41NTMgMTkuNzE2N0MxMzEuOTYzIDE5LjcxNjcgMTMwLjYyOCAyMC4yODg4IDEyOS42NzUgMjEuMzY5NkMxMjguNjU4IDIyLjQ1MDMgMTI4LjE0OSAyMy45MTI1IDEyOC4xNDkgMjUuNjkyNUMxMjguMTQ5IDI3LjQ3MjUgMTI4LjY1OCAyOC44NzExIDEyOS42NzUgMjkuOTUxOEMxMzAuNjkyIDMxLjA5NjEgMTMxLjk2MyAzMS42NjgzIDEzMy41NTMgMzEuNjY4M1oiIGZpbGw9IiM5QjlCOUIiLz4KPHBhdGggZD0iTTU3LjIwMjQgMjAuMzUyNUg0NC45MzNWMjQuNjExOEg1MS45MjU5QzUxLjczNTIgMjYuNzczMyA1MC45MDg4IDI4LjQyNjEgNDkuNTEwMiAyOS43NjExQzQ4LjExMTYgMzEuMDMyNiA0Ni4zMzE1IDMxLjY2ODMgNDQuMDQyOSAzMS42NjgzQzQyLjc3MTUgMzEuNjY4MyA0MS41NjM2IDMxLjQxNCA0MC41NDY1IDMwLjk2OUMzOS40NjU3IDMwLjUyNCAzOC41NzU3IDI5Ljg4ODMgMzcuODEyOSAyOC45OTgzQzM3LjA1IDI4LjE3MTggMzYuNDc3OCAyNy4xNTQ3IDM2LjAzMjggMjUuOTQ2OEMzNS41ODc4IDI0LjczODkgMzUuMzk3MSAyMy40Njc1IDM1LjM5NzEgMjIuMDA1M0MzNS4zOTcxIDIwLjU0MzIgMzUuNTg3OCAxOS4yNzE3IDM2LjAzMjggMTguMDYzOEMzNi40MTQzIDE2Ljg1NiAzNy4wNSAxNS45MDI0IDM3LjgxMjkgMTUuMDEyNEMzOC41NzU3IDE0LjE4NTkgMzkuNDY1NyAxMy41NTAyIDQwLjU0NjUgMTMuMDQxNkM0MS42MjcyIDEyLjU5NjYgNDIuNzcxNSAxMi4zNDIzIDQ0LjEwNjUgMTIuMzQyM0M0Ni43NzY2IDEyLjM0MjMgNDguODEwOSAxMi45NzggNTAuMjA5NSAxNC4yNDk1TDUzLjUxNTIgMTAuOTQzOEM1MS4wMzU5IDkuMDM2NTkgNDcuODU3MyA4LjAxOTQxIDQ0LjEwNjUgOC4wMTk0MUM0Mi4wMDg2IDguMDE5NDEgNDAuMTAxNSA4LjMzNzI5IDM4LjM4NSA5LjAzNjU5QzM2LjY2ODYgOS43MzU4OCAzNS4yMDY0IDEwLjYyNTkgMzMuOTk4NSAxMS44MzM3QzMyLjc5MDYgMTMuMDQxNiAzMS44MzcxIDE0LjUwMzggMzEuMjAxNCAxNi4yMjAzQzMwLjU2NTYgMTcuOTM2NyAzMC4yNDc4IDE5Ljg0MzggMzAuMjQ3OCAyMS44NzgyQzMwLjI0NzggMjMuOTEyNSAzMC41NjU2IDI1LjgxOTcgMzEuMjY0OSAyNy41MzYxQzMxLjk2NDIgMjkuMjUyNiAzMi44NTQyIDMwLjcxNDcgMzQuMDYyMSAzMS45MjI2QzM1LjI3IDMzLjEzMDUgMzYuNzMyMSAzNC4wODQxIDM4LjQ0ODYgMzQuNzE5OEM0MC4xNjUgMzUuNDE5MSA0Mi4wNzIyIDM1LjczNyA0NC4xMDY1IDM1LjczN0M0Ni4xNDA4IDM1LjczNyA0Ny45ODQ0IDM1LjQxOTEgNDkuNjM3MyAzNC43MTk4QzUxLjI5MDIgMzQuMDIwNSA1Mi42ODg4IDMzLjEzMDUgNTMuODMzMSAzMS45MjI2QzU0Ljk3NzQgMzAuNzE0NyA1NS44Njc0IDI5LjI1MjYgNTYuNTAzMSAyNy41MzYxQzU3LjEzODggMjUuODE5NyA1Ny40NTY3IDIzLjkxMjUgNTcuNDU2NyAyMS44NzgyVjIxLjA1MTdDNTcuMjY2IDIwLjkyNDYgNTcuMjAyNCAyMC42MDY3IDU3LjIwMjQgMjAuMzUyNVoiIGZpbGw9IiM5QjlCOUIiLz4KPHBhdGggZD0iTTk1Ljk4MTUgMjAuMzUyNUg4My43MTIxVjI0LjYxMThIOTAuNzA1QzkwLjUxNDMgMjYuNzczMyA4OS42ODc5IDI4LjQyNjEgODguMjg5MyAyOS43NjExQzg2Ljg5MDcgMzEuMDMyNiA4NS4xMTA2IDMxLjY2ODMgODIuODIyIDMxLjY2ODNDODEuNTUwNiAzMS42NjgzIDgwLjM0MjcgMzEuNDE0IDc5LjMyNTYgMzAuOTY5Qzc4LjI0NDggMzAuNTI0IDc3LjM1NDggMjkuODg4MyA3Ni41OTIgMjguOTk4M0M3NS44MjkxIDI4LjE3MTggNzUuMjU3IDI3LjE1NDcgNzQuODExOSAyNS45NDY4Qzc0LjM2NjkgMjQuNzM4OSA3NC4xNzYyIDIzLjQ2NzUgNzQuMTc2MiAyMi4wMDUzQzc0LjE3NjIgMjAuNTQzMiA3NC4zNjY5IDE5LjI3MTcgNzQuODExOSAxOC4wNjM4Qzc1LjE5MzQgMTYuODU2IDc1LjgyOTEgMTUuOTAyNCA3Ni41OTIgMTUuMDEyNEM3Ny4zNTQ4IDE0LjE4NTkgNzguMjQ0OCAxMy41NTAyIDc5LjMyNTYgMTMuMDQxNkM4MC40MDYzIDEyLjU5NjYgODEuNTUwNiAxMi4zNDIzIDgyLjg4NTYgMTIuMzQyM0M4NS41NTU3IDEyLjM0MjMgODcuNTkgMTIuOTc4IDg4Ljk4ODYgMTQuMjQ5NUw5Mi4yOTQzIDEwLjk0MzhDODkuODE1IDkuMDM2NTkgODYuNjM2NCA4LjAxOTQxIDgyLjg4NTYgOC4wMTk0MUM4MC43ODc4IDguMDE5NDEgNzguODgwNiA4LjMzNzI5IDc3LjE2NDEgOS4wMzY1OUM3NS40NDc3IDkuNzM1ODggNzMuOTg1NSAxMC42MjU5IDcyLjc3NzYgMTEuODMzN0M3MS41Njk4IDEzLjA0MTYgNzAuNjE2MiAxNC41MDM4IDY5Ljk4MDUgMTYuMjIwM0M2OS4zNDQ3IDE3LjkzNjcgNjkuMDI2OSAxOS44NDM4IDY5LjAyNjkgMjEuODc4MkM2OS4wMjY5IDIzLjkxMjUgNjkuMzQ0NyAyNS44MTk3IDcwLjA0NCAyNy41MzYxQzcwLjc0MzMgMjkuMjUyNiA3MS42MzM0IDMwLjcxNDcgNzIuODQxMiAzMS45MjI2Qzc0LjA0OTEgMzMuMTMwNSA3NS41MTEyIDM0LjA4NDEgNzcuMjI3NyAzNC43MTk4Qzc4Ljk0NDEgMzUuNDE5MSA4MC44NTEzIDM1LjczNyA4Mi44ODU2IDM1LjczN0M4NC45MiAzNS43MzcgODYuNzYzNiAzNS40MTkxIDg4LjQxNjQgMzQuNzE5OEM5MC4wNjkzIDM0LjAyMDUgOTEuNDY3OSAzMy4xMzA1IDkyLjYxMjIgMzEuOTIyNkM5My43NTY1IDMwLjcxNDcgOTQuNjQ2NSAyOS4yNTI2IDk1LjI4MjIgMjcuNTM2MUM5NS45MTggMjUuODE5NyA5Ni4yMzU4IDIzLjkxMjUgOTYuMjM1OCAyMS44NzgyVjIxLjA1MTdDOTYuMDQ1MSAyMC45MjQ2IDk1Ljk4MTUgMjAuNjA2NyA5NS45ODE1IDIwLjM1MjVaIiBmaWxsPSIjOUI5QjlCIi8+Cjwvc3ZnPgo='); - background-repeat: no-repeat; - background-size: 170px 40px; - display: block; - height: 40px; - width: 170px; - } +.ag-watermark::before { + content: ''; + background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjA5IiBoZWlnaHQ9IjM2IiB2aWV3Qm94PSIwIDAgMjA5IDM2IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBkPSJNMTkyLjk5MyAyMy42NTgyVjE1LjcxMTdIMTc5LjQ1MkwxNzEuNTA1IDIzLjY1ODJIMTkyLjk5M1oiIGZpbGw9IiM5QjlCOUIiLz4KPHBhdGggZD0iTTIwOC4yNSAzLjk1MDgxSDE5MS4yNzZMMTgzLjI2NiAxMS44OTczSDIwOC4yNVYzLjk1MDgxWiIgZmlsbD0iIzlCOUI5QiIvPgo8cGF0aCBkPSJNMTYzLjYyMiAzMS42MDQ4TDE2Ny42OTEgMjcuNTM2MUgxODEuNDIzVjM1LjQ4MjdIMTYzLjYyMlYzMS42MDQ4WiIgZmlsbD0iIzlCOUI5QiIvPgo8cGF0aCBkPSJNMTY2LjYxIDE5Ljc4MDNIMTc1LjM4M0wxODMuMzkzIDExLjgzMzdIMTY2LjYxVjE5Ljc4MDNaIiBmaWxsPSIjOUI5QjlCIi8+CjxwYXRoIGQ9Ik0xNTcuMDExIDMxLjYwNDdIMTYzLjYyMkwxNzEuNTA1IDIzLjY1ODJIMTU3LjAxMVYzMS42MDQ3WiIgZmlsbD0iIzlCOUI5QiIvPgo8cGF0aCBkPSJNMTkxLjI3NiAzLjk1MDgxTDE4Ny4yMDggOC4wMTk0MUgxNjEuMjdWMC4wNzI4NzZIMTkxLjI3NlYzLjk1MDgxWiIgZmlsbD0iIzlCOUI5QiIvPgo8cGF0aCBkPSJNMjAuODM5MSAzMC4yMDYxSDguMzc4OTJMNi4yMTc0NSAzNS41NDYySDAuNzUwMjQ0TDEyLjI1NjggOC41OTE1NUgxNy4wMjQ3TDI4LjUzMTMgMzUuNTQ2MkgyMy4wMDA1TDIwLjgzOTEgMzAuMjA2MVpNMTkuMTIyNyAyNS45NDY4TDE0LjYwOSAxNC45NDg4TDEwLjA5NTQgMjUuOTQ2OEgxOS4xMjI3WiIgZmlsbD0iIzlCOUI5QiIvPgo8cGF0aCBkPSJNMTA0LjQzNyAxOC41MDg5QzEwNi4wMjYgMTYuMTU2NyAxMTAuMDMxIDE1LjkwMjQgMTExLjY4NCAxNS45MDI0VjIwLjQ3OTZDMTA5LjY1IDIwLjQ3OTYgMTA3LjYxNSAyMC41NDMyIDEwNi40MDcgMjEuNDMzMkMxMDUuMiAyMi4zMjMyIDEwNC41NjQgMjMuNTMxMSAxMDQuNTY0IDI0Ljk5MzJWMzUuNTQ2Mkg5OS42MDUxVjE1LjkwMjRIMTA0LjM3M0wxMDQuNDM3IDE4LjUwODlaIiBmaWxsPSIjOUI5QjlCIi8+CjxwYXRoIGQ9Ik0xMTkuMzc2IDE1LjkwMjRIMTE0LjQxOFYzNS41NDYySDExOS4zNzZWMTUuOTAyNFoiIGZpbGw9IiM5QjlCOUIiLz4KPHBhdGggZD0iTTExOS4zNzYgNy4xMjkzOUgxMTQuNDE4VjEyLjk3OEgxMTkuMzc2VjcuMTI5MzlaIiBmaWxsPSIjOUI5QjlCIi8+CjxwYXRoIGQ9Ik0xNDMuOTc5IDcuMTI5MzlWMzUuNTQ2MkgxMzkuMjExTDEzOS4wODQgMzIuNTU4M0MxMzguMzg0IDMzLjU3NTUgMTM3LjQ5NCAzNC40MDE5IDEzNi40MTQgMzUuMDM3NkMxMzUuMzMzIDM1LjYwOTggMTMzLjk5OCAzNS45Mjc2IDEzMi40NzIgMzUuOTI3NkMxMzEuMTM3IDM1LjkyNzYgMTI5Ljg2NiAzNS42NzMzIDEyOC43ODUgMzUuMjI4M0MxMjcuNjQxIDM0LjcxOTcgMTI2LjYyMyAzNC4wODQgMTI1Ljc5NyAzMy4xOTRDMTI0Ljk3MSAzMi4zMDQgMTI0LjI3MSAzMS4yMjMzIDEyMy44MjYgMzAuMDE1NEMxMjMuMzE4IDI4LjgwNzUgMTIzLjEyNyAyNy40MDkgMTIzLjEyNyAyNS44ODMyQzEyMy4xMjcgMjQuMzU3NSAxMjMuMzgxIDIyLjk1ODkgMTIzLjgyNiAyMS42ODc0QzEyNC4zMzUgMjAuNDE2IDEyNC45NzEgMTkuMzM1MyAxMjUuNzk3IDE4LjQ0NTNDMTI2LjYyMyAxNy41NTUyIDEyNy42NDEgMTYuODU2IDEyOC43ODUgMTYuMzQ3NEMxMjkuOTI5IDE1LjgzODggMTMxLjEzNyAxNS41ODQ1IDEzMi40NzIgMTUuNTg0NUMxMzMuOTk4IDE1LjU4NDUgMTM1LjI2OSAxNS44Mzg4IDEzNi4zNSAxNi40MTA5QzEzNy40MzEgMTYuOTgzMSAxMzguMzIxIDE3Ljc0NTkgMTM5LjAyIDE4LjgyNjdWNy4xOTI5NUgxNDMuOTc5VjcuMTI5MzlaTTEzMy41NTMgMzEuNjY4M0MxMzUuMjA2IDMxLjY2ODMgMTM2LjQ3NyAzMS4wOTYyIDEzNy40OTQgMzAuMDE1NEMxMzguNTExIDI4LjkzNDcgMTM5LjAyIDI3LjQ3MjUgMTM5LjAyIDI1LjY5MjVDMTM5LjAyIDIzLjkxMjUgMTM4LjUxMSAyMi41MTM5IDEzNy40OTQgMjEuMzY5NkMxMzYuNDc3IDIwLjI4ODggMTM1LjIwNiAxOS43MTY3IDEzMy41NTMgMTkuNzE2N0MxMzEuOTYzIDE5LjcxNjcgMTMwLjYyOCAyMC4yODg4IDEyOS42NzUgMjEuMzY5NkMxMjguNjU4IDIyLjQ1MDMgMTI4LjE0OSAyMy45MTI1IDEyOC4xNDkgMjUuNjkyNUMxMjguMTQ5IDI3LjQ3MjUgMTI4LjY1OCAyOC44NzExIDEyOS42NzUgMjkuOTUxOEMxMzAuNjkyIDMxLjA5NjEgMTMxLjk2MyAzMS42NjgzIDEzMy41NTMgMzEuNjY4M1oiIGZpbGw9IiM5QjlCOUIiLz4KPHBhdGggZD0iTTU3LjIwMjQgMjAuMzUyNUg0NC45MzNWMjQuNjExOEg1MS45MjU5QzUxLjczNTIgMjYuNzczMyA1MC45MDg4IDI4LjQyNjEgNDkuNTEwMiAyOS43NjExQzQ4LjExMTYgMzEuMDMyNiA0Ni4zMzE1IDMxLjY2ODMgNDQuMDQyOSAzMS42NjgzQzQyLjc3MTUgMzEuNjY4MyA0MS41NjM2IDMxLjQxNCA0MC41NDY1IDMwLjk2OUMzOS40NjU3IDMwLjUyNCAzOC41NzU3IDI5Ljg4ODMgMzcuODEyOSAyOC45OTgzQzM3LjA1IDI4LjE3MTggMzYuNDc3OCAyNy4xNTQ3IDM2LjAzMjggMjUuOTQ2OEMzNS41ODc4IDI0LjczODkgMzUuMzk3MSAyMy40Njc1IDM1LjM5NzEgMjIuMDA1M0MzNS4zOTcxIDIwLjU0MzIgMzUuNTg3OCAxOS4yNzE3IDM2LjAzMjggMTguMDYzOEMzNi40MTQzIDE2Ljg1NiAzNy4wNSAxNS45MDI0IDM3LjgxMjkgMTUuMDEyNEMzOC41NzU3IDE0LjE4NTkgMzkuNDY1NyAxMy41NTAyIDQwLjU0NjUgMTMuMDQxNkM0MS42MjcyIDEyLjU5NjYgNDIuNzcxNSAxMi4zNDIzIDQ0LjEwNjUgMTIuMzQyM0M0Ni43NzY2IDEyLjM0MjMgNDguODEwOSAxMi45NzggNTAuMjA5NSAxNC4yNDk1TDUzLjUxNTIgMTAuOTQzOEM1MS4wMzU5IDkuMDM2NTkgNDcuODU3MyA4LjAxOTQxIDQ0LjEwNjUgOC4wMTk0MUM0Mi4wMDg2IDguMDE5NDEgNDAuMTAxNSA4LjMzNzI5IDM4LjM4NSA5LjAzNjU5QzM2LjY2ODYgOS43MzU4OCAzNS4yMDY0IDEwLjYyNTkgMzMuOTk4NSAxMS44MzM3QzMyLjc5MDYgMTMuMDQxNiAzMS44MzcxIDE0LjUwMzggMzEuMjAxNCAxNi4yMjAzQzMwLjU2NTYgMTcuOTM2NyAzMC4yNDc4IDE5Ljg0MzggMzAuMjQ3OCAyMS44NzgyQzMwLjI0NzggMjMuOTEyNSAzMC41NjU2IDI1LjgxOTcgMzEuMjY0OSAyNy41MzYxQzMxLjk2NDIgMjkuMjUyNiAzMi44NTQyIDMwLjcxNDcgMzQuMDYyMSAzMS45MjI2QzM1LjI3IDMzLjEzMDUgMzYuNzMyMSAzNC4wODQxIDM4LjQ0ODYgMzQuNzE5OEM0MC4xNjUgMzUuNDE5MSA0Mi4wNzIyIDM1LjczNyA0NC4xMDY1IDM1LjczN0M0Ni4xNDA4IDM1LjczNyA0Ny45ODQ0IDM1LjQxOTEgNDkuNjM3MyAzNC43MTk4QzUxLjI5MDIgMzQuMDIwNSA1Mi42ODg4IDMzLjEzMDUgNTMuODMzMSAzMS45MjI2QzU0Ljk3NzQgMzAuNzE0NyA1NS44Njc0IDI5LjI1MjYgNTYuNTAzMSAyNy41MzYxQzU3LjEzODggMjUuODE5NyA1Ny40NTY3IDIzLjkxMjUgNTcuNDU2NyAyMS44NzgyVjIxLjA1MTdDNTcuMjY2IDIwLjkyNDYgNTcuMjAyNCAyMC42MDY3IDU3LjIwMjQgMjAuMzUyNVoiIGZpbGw9IiM5QjlCOUIiLz4KPHBhdGggZD0iTTk1Ljk4MTUgMjAuMzUyNUg4My43MTIxVjI0LjYxMThIOTAuNzA1QzkwLjUxNDMgMjYuNzczMyA4OS42ODc5IDI4LjQyNjEgODguMjg5MyAyOS43NjExQzg2Ljg5MDcgMzEuMDMyNiA4NS4xMTA2IDMxLjY2ODMgODIuODIyIDMxLjY2ODNDODEuNTUwNiAzMS42NjgzIDgwLjM0MjcgMzEuNDE0IDc5LjMyNTYgMzAuOTY5Qzc4LjI0NDggMzAuNTI0IDc3LjM1NDggMjkuODg4MyA3Ni41OTIgMjguOTk4M0M3NS44MjkxIDI4LjE3MTggNzUuMjU3IDI3LjE1NDcgNzQuODExOSAyNS45NDY4Qzc0LjM2NjkgMjQuNzM4OSA3NC4xNzYyIDIzLjQ2NzUgNzQuMTc2MiAyMi4wMDUzQzc0LjE3NjIgMjAuNTQzMiA3NC4zNjY5IDE5LjI3MTcgNzQuODExOSAxOC4wNjM4Qzc1LjE5MzQgMTYuODU2IDc1LjgyOTEgMTUuOTAyNCA3Ni41OTIgMTUuMDEyNEM3Ny4zNTQ4IDE0LjE4NTkgNzguMjQ0OCAxMy41NTAyIDc5LjMyNTYgMTMuMDQxNkM4MC40MDYzIDEyLjU5NjYgODEuNTUwNiAxMi4zNDIzIDgyLjg4NTYgMTIuMzQyM0M4NS41NTU3IDEyLjM0MjMgODcuNTkgMTIuOTc4IDg4Ljk4ODYgMTQuMjQ5NUw5Mi4yOTQzIDEwLjk0MzhDODkuODE1IDkuMDM2NTkgODYuNjM2NCA4LjAxOTQxIDgyLjg4NTYgOC4wMTk0MUM4MC43ODc4IDguMDE5NDEgNzguODgwNiA4LjMzNzI5IDc3LjE2NDEgOS4wMzY1OUM3NS40NDc3IDkuNzM1ODggNzMuOTg1NSAxMC42MjU5IDcyLjc3NzYgMTEuODMzN0M3MS41Njk4IDEzLjA0MTYgNzAuNjE2MiAxNC41MDM4IDY5Ljk4MDUgMTYuMjIwM0M2OS4zNDQ3IDE3LjkzNjcgNjkuMDI2OSAxOS44NDM4IDY5LjAyNjkgMjEuODc4MkM2OS4wMjY5IDIzLjkxMjUgNjkuMzQ0NyAyNS44MTk3IDcwLjA0NCAyNy41MzYxQzcwLjc0MzMgMjkuMjUyNiA3MS42MzM0IDMwLjcxNDcgNzIuODQxMiAzMS45MjI2Qzc0LjA0OTEgMzMuMTMwNSA3NS41MTEyIDM0LjA4NDEgNzcuMjI3NyAzNC43MTk4Qzc4Ljk0NDEgMzUuNDE5MSA4MC44NTEzIDM1LjczNyA4Mi44ODU2IDM1LjczN0M4NC45MiAzNS43MzcgODYuNzYzNiAzNS40MTkxIDg4LjQxNjQgMzQuNzE5OEM5MC4wNjkzIDM0LjAyMDUgOTEuNDY3OSAzMy4xMzA1IDkyLjYxMjIgMzEuOTIyNkM5My43NTY1IDMwLjcxNDcgOTQuNjQ2NSAyOS4yNTI2IDk1LjI4MjIgMjcuNTM2MUM5NS45MTggMjUuODE5NyA5Ni4yMzU4IDIzLjkxMjUgOTYuMjM1OCAyMS44NzgyVjIxLjA1MTdDOTYuMDQ1MSAyMC45MjQ2IDk1Ljk4MTUgMjAuNjA2NyA5NS45ODE1IDIwLjM1MjVaIiBmaWxsPSIjOUI5QjlCIi8+Cjwvc3ZnPgo='); + background-repeat: no-repeat; + background-size: 170px 40px; + display: block; + height: 40px; + width: 170px; } .ag-watermark-text { diff --git a/packages/ag-grid-enterprise/src/sideBar/agSideBar.css b/packages/ag-grid-enterprise/src/sideBar/agSideBar.css index 90fde5f71e0..05053dbd0d0 100644 --- a/packages/ag-grid-enterprise/src/sideBar/agSideBar.css +++ b/packages/ag-grid-enterprise/src/sideBar/agSideBar.css @@ -24,10 +24,6 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - - > * { - flex: none; - } } .ag-tool-panel-horizontal-resize { @@ -75,34 +71,34 @@ /* hide button border on top button if it's flush with container top, which has its own border */ margin-top: -1px; +} - &::before { - content: ''; - display: block; - position: absolute; - left: 0; - top: 0; - bottom: 0; - width: var(--ag-side-button-selected-underline-width); - background-color: transparent; - transition: background-color var(--ag-side-button-selected-underline-transition-duration); - } +.ag-side-button::before { + content: ''; + display: block; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: var(--ag-side-button-selected-underline-width); + background-color: transparent; + transition: background-color var(--ag-side-button-selected-underline-transition-duration); +} - &:hover { - background-color: var(--ag-side-button-hover-background-color); - color: var(--ag-side-button-hover-text-color); - } +.ag-side-button:hover { + background-color: var(--ag-side-button-hover-background-color); + color: var(--ag-side-button-hover-text-color); +} - &.ag-selected { - color: var(--ag-side-button-selected-text-color); - background-color: var(--ag-side-button-selected-background-color); - border-top: var(--ag-side-button-selected-border); - border-bottom: var(--ag-side-button-selected-border); +.ag-side-button.ag-selected { + color: var(--ag-side-button-selected-text-color); + background-color: var(--ag-side-button-selected-background-color); + border-top: var(--ag-side-button-selected-border); + border-bottom: var(--ag-side-button-selected-border); +} - &::before { - background-color: var(--ag-side-button-selected-underline-color); - } - } +.ag-side-button.ag-selected::before { + background-color: var(--ag-side-button-selected-underline-color); } .ag-side-button-button { diff --git a/packages/ag-grid-enterprise/src/widgets/agRichSelect.css b/packages/ag-grid-enterprise/src/widgets/agRichSelect.css index 028243baed9..9f9fcffe98c 100644 --- a/packages/ag-grid-enterprise/src/widgets/agRichSelect.css +++ b/packages/ag-grid-enterprise/src/widgets/agRichSelect.css @@ -108,9 +108,9 @@ border: none; padding: 0; margin-left: var(--ag-spacing); +} - &:hover { - cursor: pointer; - color: var(--ag-accent-color); - } +.ag-pill-button:hover { + cursor: pointer; + color: var(--ag-accent-color); } diff --git a/packages/ag-grid-enterprise/src/widgets/pillDropZonePanel.css b/packages/ag-grid-enterprise/src/widgets/pillDropZonePanel.css index 42f4c2f99fb..fb9e5f87e3a 100644 --- a/packages/ag-grid-enterprise/src/widgets/pillDropZonePanel.css +++ b/packages/ag-grid-enterprise/src/widgets/pillDropZonePanel.css @@ -63,10 +63,6 @@ padding-bottom: var(--ag-spacing); padding-right: var(--ag-spacing); padding-left: var(--ag-spacing); - - > * { - flex: none; - } } :where(.ag-column-drop-empty) .ag-column-drop-vertical-list { @@ -78,10 +74,10 @@ margin-right: calc(var(--ag-spacing) / 4); min-width: 0; opacity: 0.75; +} - &:hover { - opacity: 1; - } +.ag-column-drop-cell-button:hover { + opacity: 1; } .ag-column-drop-cell-drag-handle { @@ -135,10 +131,10 @@ .ag-select-agg-func-virtual-list-item { cursor: default; padding-left: calc(var(--ag-spacing) * 2); +} - &:hover { - background-color: var(--ag-selected-row-background-color); - } +.ag-select-agg-func-virtual-list-item:hover { + background-color: var(--ag-selected-row-background-color); } .ag-column-drop-horizontal-half-width:where(:not(:last-child)) { diff --git a/plugins/stylelint-plugin-ag/LOW_PERFORMANCE_SELECTORS.md b/plugins/stylelint-plugin-ag/LOW_PERFORMANCE_SELECTORS.md new file mode 100644 index 00000000000..d9206f08d05 --- /dev/null +++ b/plugins/stylelint-plugin-ag/LOW_PERFORMANCE_SELECTORS.md @@ -0,0 +1,166 @@ +# Low Performance CSS Selectors + +This document explains the `ag/no-low-performance-key-selector` lint rule and how to fix violations. + +## Why this is an issue for AG Grid + +Our customers often put the grid into applications that push the boundaries of the number of DOM elements a browser page can contain. Many grid applications have hundreds of thousands of DOM elements, most of them outside of the grid. + +But the way CSS works, it is very easy to write a selector that requires checking every DOM element on the page, even those not part of a grid, to see if they match. + +To prevent slowing down our customers' applications, we need to carefully write our CSS in such a way that it does not have to be checked against every element on the page. + +## Background: right to left matching and key selectors + +When the browser matches a CSS selector, it starts from the **key selector** (the rightmost part) and works leftward. A "low performance" key selector forces the browser to check many elements before narrowing down candidates. + +The key selector is the rightmost or innermost bit of CSS selector, up to a nesting boundary or the descendant combinator (ie a space between rule parts). + +Examples: + +```css +/* ".bar" is key selector, ".foo" is not part of it because of + the space (descendent combinator) */ +.foo .bar { + color: red; +} +/* ".foo" and ".bar" are key selectors - ", " is a list separator + not a descendent combinator */ +.foo, +.bar { + color: red; +} +/* ".foo" and ".bar" and ":after" are all key selectors - This is + counterintuitive since the "&" has the semantic effect of joining + it directly to the parents, like ".foo:after, .bar:after" But that + is not how Chrome treats it. */ +.foo, +.bar { + &:after { + content: ''; + } +} +``` + +## High-performance CSS + +The rule for high-performance CSS is that **the key selector should be fast to match, by containing a class or ID**. + +The classic slow selector is `.foo > *` which feels like it should be fast to match because it's saying "Select all children of the .foo element". But in fact what it does with right to left matching is first select literally every element in the page, and then narrow them down by whether they have a .foo parent. This is slow. + +Browsers aren't very smart here, so it's often possible to take a low-performance selector and make it high-performance by doing a simple transformation, often involving duplicating some code, to transform it into a high-performance selector. + +## Categories and Fixes + +### 1. Nested Pseudo-Elements (`&:after`, `&:before`) + +**Problem:** + +```css +.foo { + &:after { + content: ''; + } /* key: &:after - LOW PERFORMANCE */ +} +``` + +**Fix:** Move the class into the key selector: + +```css +.foo:after { + content: ''; +} /* key: .foo:after - HIGH PERFORMANCE (has .foo) */ +``` + +--- + +### 3. `:where()` with Selector Lists + +**Problem:** + +`:where()` is used to lower the specificity of a selector. When it contains a single high performance selector, it is also high performance. But when it contains a list of selectors it is low performance, _even if the list contains only high performance selectors_. + +```css +:where(.foo, .bar, .baz) { + color: red; +} /* LOW PERFORMANCE - multiple selectors inside :where */ +``` + +**Fix:** Split into separate `:where()` calls: + +```css +:where(.foo), +:where(.bar), +:where(.baz) { + color: red; +} /* HIGH PERFORMANCE - each has single class */ +``` + +--- + +### 4. Type Selectors (`div`, `span`, `input`) + +**Problem:** + +Under right-to-left matching, these match every element of the specified type in the page, then narrow by the parent selector part: + +```css +.container > div { + margin: 10px; +} /* key: div - LOW PERFORMANCE */ +``` + +**Fix:** Add a class to the element: + +```css +.container > .container-item { + margin: 10px; +} /* key: .container-item - HIGH PERFORMANCE */ +``` + +--- + +### 5. Universal Selector (`*`) + +**Problem:** + +```css +.foo > * { + box-sizing: border-box; +} /* key: * - LOW PERFORMANCE */ +``` + +**Fix:** Target specific classes or use inheritance where possible: + +```css +.foo-child { + box-sizing: border-box; +} /* key: .foo-child - HIGH PERFORMANCE */ +``` + +--- + +### 6. `:not()` and `:has()` Selectors + +These are always low performance on their own because they require checking every element on the page against a condition. + +- `:not()` matches broadly (everything that ISN'T something) +- `:has()` requires checking descendants + +**Problem:** + +```css +.list :not(.disabled) { + opacity: 1; +} /* key: :not(.disabled) - LOW PERFORMANCE */ +``` + +**Fix:** This is a hidden universal selector - `:not(.disabled)` is effectively +`*:not(.disabled)`, and the fix is the same - include a class in addition to +`:not` so that it doesn't first match everything and then narrow down: + +```css +.list .item:not(.disabled) { + opacity: 1; +} /* key: .item:not(.disabled) - HIGH PERFORMANCE */ +``` diff --git a/plugins/stylelint-plugin-ag/index.mjs b/plugins/stylelint-plugin-ag/index.mjs new file mode 100644 index 00000000000..b8aaef68c51 --- /dev/null +++ b/plugins/stylelint-plugin-ag/index.mjs @@ -0,0 +1,3 @@ +import noLowPerformanceKeySelector from './rules/no-low-performance-key-selector.mjs'; + +export default [noLowPerformanceKeySelector]; diff --git a/plugins/stylelint-plugin-ag/jest.config.js b/plugins/stylelint-plugin-ag/jest.config.js new file mode 100644 index 00000000000..c2a6092868e --- /dev/null +++ b/plugins/stylelint-plugin-ag/jest.config.js @@ -0,0 +1,6 @@ +module.exports = { + displayName: 'stylelint-plugin-ag', + testEnvironment: 'node', + testMatch: ['/**/*.test.mjs'], + transform: {}, +}; diff --git a/plugins/stylelint-plugin-ag/project.json b/plugins/stylelint-plugin-ag/project.json new file mode 100644 index 00000000000..c1fed8ec5f0 --- /dev/null +++ b/plugins/stylelint-plugin-ag/project.json @@ -0,0 +1,26 @@ +{ + "name": "stylelint-plugin-ag", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "plugins/stylelint-plugin-ag", + "projectType": "library", + "targets": { + "test": { + "executor": "nx:run-commands", + "inputs": ["default", "{projectRoot}/**/*.mjs"], + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "command": "npx jest --config {projectRoot}/jest.config.js", + "env": { + "NODE_OPTIONS": "--experimental-vm-modules" + } + } + }, + "lint": { + "command": "eslint", + "options": { + "cwd": "{projectRoot}" + } + } + }, + "tags": [] +} diff --git a/plugins/stylelint-plugin-ag/rules/no-low-performance-key-selector.mjs b/plugins/stylelint-plugin-ag/rules/no-low-performance-key-selector.mjs new file mode 100644 index 00000000000..db6b50f1600 --- /dev/null +++ b/plugins/stylelint-plugin-ag/rules/no-low-performance-key-selector.mjs @@ -0,0 +1,199 @@ +import selectorParser from 'postcss-selector-parser'; +import stylelint from 'stylelint'; + +const { + createPlugin, + utils: { report, ruleMessages, validateOptions }, +} = stylelint; + +const ruleName = 'ag/no-low-performance-key-selector'; + +const messages = ruleMessages(ruleName, { + rejected: (keySelector) => + `Low-performance key selector "${keySelector}". See LOW_PERFORMANCE_SELECTORS.md for fixing advice.`, + rejectedMultiple: (count, selectors) => + `${count} Low-performance key selectors: ${selectors}. See LOW_PERFORMANCE_SELECTORS.md for fixing advice.`, +}); + +const meta = { + url: 'https://github.com/ag-grid/ag-grid', +}; + +// Combinator types that separate compound selectors +// Note: "nesting" (&) is NOT a combinator - it's part of the compound selector +const COMBINATOR_TYPES = new Set(['combinator']); + +// Don't consider these pseudo-elements or pseudo-classes low performance, +// because they match very fast and won't require checking every element +const HIGH_PERF_PSEUDOS = new Set([ + '::-webkit-outer-spin-button', + '::-webkit-inner-spin-button', + '::-webkit-slider-runnable-track', + '::-webkit-slider-thumb', + '::-moz-range-track', + '::-moz-ag-range-thumb', + '::placeholder', + ':disabled', + ':invalid', + ':active', + ':focus', + ':focus-visible', + /* NOTE: do not add :hover here, it feels like it should be high-perf, but + actually causes slow-path matching against every element in the DOM */ +]); + +/** + * Check if a pseudo selector is high-performance. + * This includes whitelisted pseudos and :where/:is containing a single high-perf selector. + */ +function pseudoIsHighPerformance(pseudo) { + const name = pseudo.value.toLowerCase(); + + if (HIGH_PERF_PSEUDOS.has(name)) { + return true; + } + + // Check if :where/:is contains a single high-perf selector + if (name !== ':where' && name !== ':is') { + return false; + } + + // Get the selectors inside the pseudo + const innerSelectors = pseudo.nodes; + if (!innerSelectors || innerSelectors.length !== 1) { + return false; + } + + const innerSelector = innerSelectors[0]; + if (innerSelector.type !== 'selector') { + return false; + } + + // Recursively check if the inner selector is high-performance + return isHighPerformance(innerSelector.nodes); +} + +/** + * Check if a compound selector (array of nodes) is high performance. + * High performance means it contains a class, ID, standalone nesting selector (&), + * :where/:is with a single high-perf selector, or whitelisted pseudos. + * + * Note: "&" alone is high-perf (resolves to parent class), but "&:after" is NOT + * because Chrome treats the nested pseudo-element differently. + */ +function isHighPerformance(nodes) { + for (const node of nodes) { + if (node.type === 'class' || node.type === 'id') { + return true; + } + // Custom elements (containing dashes) are high-performance + if (node.type === 'tag' && node.value.includes('-')) { + return true; + } + if (node.type === 'pseudo' && pseudoIsHighPerformance(node)) { + return true; + } + } + // Standalone "&" is high-perf (resolves to parent selector, if the parent + // selector is not high perf it'll get flagged separately) + if (nodes.length === 1 && nodes[0].type === 'nesting') { + return true; + } + return false; +} + +/** + * Extract the key selector nodes from a parsed selector. + * The key selector is the rightmost compound selector after the last combinator or nesting boundary. + */ +function extractKeySelector(selector) { + const nodes = selector.nodes; + let lastCombinatorIndex = -1; + + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + if (COMBINATOR_TYPES.has(node.type)) { + lastCombinatorIndex = i; + } + } + + // Key selector is everything after the last combinator (or the whole thing if no combinator) + return nodes.slice(lastCombinatorIndex + 1); +} + +/** + * Convert key selector nodes to a string representation. + */ +function keySelectorToString(nodes) { + return nodes.map((n) => n.toString()).join(''); +} + +const ruleFunction = (primary) => { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: primary, + }); + + if (!validOptions) { + return; + } + + root.walkRules((rule) => { + // Skip keyframes rules + if (rule.parent && rule.parent.type === 'atrule' && /^(-webkit-)?keyframes$/i.test(rule.parent.name)) { + return; + } + + let parsed; + try { + parsed = selectorParser().astSync(rule.selector); + } catch { + // Skip invalid selectors + return; + } + + // Collect all low-performance key selectors in this rule + const lowPerfSelectors = []; + + parsed.each((selector) => { + if (selector.type !== 'selector') { + return; + } + + const keyNodes = extractKeySelector(selector); + if (keyNodes.length === 0) { + return; + } + + if (!isHighPerformance(keyNodes)) { + const keyString = keySelectorToString(keyNodes); + lowPerfSelectors.push(keyString); + } + }); + + // Report once per rule, listing all low-performance selectors + if (lowPerfSelectors.length > 0) { + const message = + lowPerfSelectors.length === 1 + ? messages.rejected(lowPerfSelectors[0]) + : messages.rejectedMultiple( + lowPerfSelectors.length, + lowPerfSelectors.map((s) => `"${s.trim()}"`).join(', ') + ); + + report({ + message, + node: rule, + result, + ruleName, + }); + } + }); + }; +}; + +ruleFunction.ruleName = ruleName; +ruleFunction.messages = messages; +ruleFunction.meta = meta; + +export default createPlugin(ruleName, ruleFunction); diff --git a/plugins/stylelint-plugin-ag/rules/no-low-performance-key-selector.test.mjs b/plugins/stylelint-plugin-ag/rules/no-low-performance-key-selector.test.mjs new file mode 100644 index 00000000000..7805f27caa7 --- /dev/null +++ b/plugins/stylelint-plugin-ag/rules/no-low-performance-key-selector.test.mjs @@ -0,0 +1,184 @@ +import stylelint from 'stylelint'; + +import plugin from './no-low-performance-key-selector.mjs'; + +const config = { + plugins: [plugin], + rules: { + 'ag/no-low-performance-key-selector': true, + }, +}; + +async function lint(code) { + const result = await stylelint.lint({ + code, + config, + }); + return result.results[0].warnings; +} + +describe('no-low-performance-key-selector', () => { + describe('should flag (low performance key selector)', () => { + it('flags nested pseudo-element: .foo { &:after { } }', async () => { + const warnings = await lint('.foo { &:after { color: red } }'); + expect(warnings).toHaveLength(1); + expect(warnings[0].text).toContain('&:after'); + }); + + it('flags nested pseudo-class: .foo { &:hover { } }', async () => { + const warnings = await lint('.foo { &:hover { color: red } }'); + expect(warnings).toHaveLength(1); + expect(warnings[0].text).toContain('&:hover'); + }); + + it('flags universal selector: .foo > * { }', async () => { + const warnings = await lint('.foo > * { color: red }'); + expect(warnings).toHaveLength(1); + expect(warnings[0].text).toContain('*'); + }); + + it('flags type selector after combinator: .foo > div { }', async () => { + const warnings = await lint('.foo > div { color: red }'); + expect(warnings).toHaveLength(1); + expect(warnings[0].text).toContain('div'); + }); + + it('flags bare type selector: div { }', async () => { + const warnings = await lint('div { color: red }'); + expect(warnings).toHaveLength(1); + expect(warnings[0].text).toContain('div'); + }); + + it('flags attribute selector: [data-x] { }', async () => { + const warnings = await lint('[data-x] { color: red }'); + expect(warnings).toHaveLength(1); + expect(warnings[0].text).toContain('[data-x]'); + }); + + it('flags :where with selector list: :where(.foo, .bar) { }', async () => { + const warnings = await lint(':where(.foo, .bar) { color: red }'); + expect(warnings).toHaveLength(1); + expect(warnings[0].text).toContain(':where(.foo, .bar)'); + }); + + it('flags nested :where with selector list: .foo { :where(.bar, .baz) { } }', async () => { + const warnings = await lint('.foo { :where(.bar, .baz) { color: red } }'); + expect(warnings).toHaveLength(1); + expect(warnings[0].text).toContain(':where(.bar, .baz)'); + }); + + it('flags :not: :not(.foo) { }', async () => { + const warnings = await lint(':not(.foo) { color: red }'); + expect(warnings).toHaveLength(1); + expect(warnings[0].text).toContain(':not(.foo)'); + }); + + it('flags :has: :has(.foo) { }', async () => { + const warnings = await lint(':has(.foo) { color: red }'); + expect(warnings).toHaveLength(1); + expect(warnings[0].text).toContain(':has(.foo)'); + }); + + it('flags deeply nested: .foo { .bar { &:after { } } }', async () => { + const warnings = await lint('.foo { .bar { &:after { color: red } } }'); + expect(warnings).toHaveLength(1); + expect(warnings[0].text).toContain('&:after'); + }); + + it('flags only the bad selector in selector list: .foo, div { }', async () => { + const warnings = await lint('.foo, div { color: red }'); + expect(warnings).toHaveLength(1); + expect(warnings[0].text).toContain('div'); + }); + + it('flags nesting containers even without declarations', async () => { + const warnings = await lint(":where(.a, .b) { :after { content: ''; } }"); + // Both should be flagged - nesting containers still cause slow matching + expect(warnings).toHaveLength(2); + expect(warnings[0].text).toContain(':where(.a, .b)'); + expect(warnings[1].text).toContain(':after'); + }); + }); + + describe('should NOT flag (high performance key selector)', () => { + it('allows class with pseudo-element: .foo:after { }', async () => { + const warnings = await lint('.foo:after { }'); + expect(warnings).toHaveLength(0); + }); + + it('allows class with pseudo-class: .foo:hover { }', async () => { + const warnings = await lint('.foo:hover { }'); + expect(warnings).toHaveLength(0); + }); + + it('allows class after combinator: .foo > .bar { }', async () => { + const warnings = await lint('.foo > .bar { }'); + expect(warnings).toHaveLength(0); + }); + + it('allows nested class: .foo { &.bar { } }', async () => { + const warnings = await lint('.foo { &.bar { } }'); + expect(warnings).toHaveLength(0); + }); + + it('allows nested descendant class: .foo { & .bar { } }', async () => { + const warnings = await lint('.foo { & .bar { } }'); + expect(warnings).toHaveLength(0); + }); + + it('allows :where with single class and pseudo: .foo { &:where(.bar):after { } }', async () => { + const warnings = await lint('.foo { &:where(.bar):after { } }'); + expect(warnings).toHaveLength(0); + }); + + it('allows ID selector: #main { }', async () => { + const warnings = await lint('#main { }'); + expect(warnings).toHaveLength(0); + }); + + it('allows type with class: div.foo { }', async () => { + const warnings = await lint('div.foo { }'); + expect(warnings).toHaveLength(0); + }); + + it('allows :where with single class: :where(.foo) { }', async () => { + const warnings = await lint(':where(.foo) { }'); + expect(warnings).toHaveLength(0); + }); + + it('allows selector list of single-class :where: :where(.foo), :where(.bar) { }', async () => { + const warnings = await lint(':where(.foo), :where(.bar) { }'); + expect(warnings).toHaveLength(0); + }); + + it('allows nested class under :where: :where(.foo) { .bar { } }', async () => { + const warnings = await lint(':where(.foo) { .bar { } }'); + expect(warnings).toHaveLength(0); + }); + + it('allows class with attribute: .foo[data-x] { }', async () => { + const warnings = await lint('.foo[data-x] { }'); + expect(warnings).toHaveLength(0); + }); + + it('allows nested class chained: .foo { &.bar:after { } }', async () => { + const warnings = await lint('.foo { &.bar:after { } }'); + expect(warnings).toHaveLength(0); + }); + + it('allows bare class: .foo { }', async () => { + const warnings = await lint('.foo { }'); + expect(warnings).toHaveLength(0); + }); + + it('allows :is with single class: :is(.foo) { }', async () => { + const warnings = await lint(':is(.foo) { }'); + expect(warnings).toHaveLength(0); + }); + + it('allows standalone nesting selector: .foo { :where(.bar) & { } }', async () => { + const warnings = await lint('.foo { :where(.bar) & { color: red } }'); + expect(warnings).toHaveLength(0); + }); + }); +}); diff --git a/utilities/all/project.json b/utilities/all/project.json index 77459aff181..defe7201022 100644 --- a/utilities/all/project.json +++ b/utilities/all/project.json @@ -257,7 +257,8 @@ "testing/behavioural", "testing/accessibility", "testing/csp", - "ag-grid-generate-example-files" + "ag-grid-generate-example-files", + "stylelint-plugin-ag" ], "tags": [] } From a1949557804b0a518e837d88bd8b982ea437f164 Mon Sep 17 00:00:00 2001 From: Tak Tran Date: Mon, 19 Jan 2026 11:38:55 +0000 Subject: [PATCH 09/12] AG-3390 - Setup nx GHA nodejs based on package.json (#12916) * git subrepo push external/ag-website-shared subrepo: subdir: "external/ag-website-shared" merged: "ae945f97001" upstream: origin: "git@github.com:ag-grid/ag-website-shared.git" branch: "latest" commit: "ae945f97001" git-subrepo: version: "0.4.9" origin: "https://github.com/ingydotnet/git-subrepo" commit: "30db3b8" * AG-3390 - Use package.json for gha node version --- .github/actions/setup-nx/action.yml | 2 +- external/ag-website-shared/.gitrepo | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/setup-nx/action.yml b/.github/actions/setup-nx/action.yml index 9bc9f866d42..117856d98e3 100644 --- a/.github/actions/setup-nx/action.yml +++ b/.github/actions/setup-nx/action.yml @@ -143,7 +143,7 @@ runs: id: setup_node uses: actions/setup-node@v4 with: - node-version: '22' + node-version-file: package.json - name: yarn install if: inputs.cache_mode == 'rw' && inputs.yarn_postinstall == 'true' diff --git a/external/ag-website-shared/.gitrepo b/external/ag-website-shared/.gitrepo index c3c0b52d90a..5051e74bd5d 100644 --- a/external/ag-website-shared/.gitrepo +++ b/external/ag-website-shared/.gitrepo @@ -6,7 +6,7 @@ [subrepo] remote = git@github.com:ag-grid/ag-website-shared.git branch = latest - commit = 003c5fd1401692753d61d7000f9cb4e74acb3fd7 - parent = 62335acdb581e9c162bb4f0d4d2a0ded1ec2673e + commit = ae945f9700101f42fc4c02bcead70bde238d1594 + parent = 795d2a5a4985c1c250235d97b092c3657d7de3a9 method = rebase cmdver = 0.4.9 From 6205c3df086be1ad862bc76a7ece471071c4b37e Mon Sep 17 00:00:00 2001 From: seanlandsman Date: Mon, 19 Jan 2026 12:36:24 +0000 Subject: [PATCH 10/12] Resolve snyk issues (#12917) --- package.json | 2 ++ yarn.lock | 33 +++++++++++++++++++-------------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 12c963c4ec7..c1fffd6e4fb 100644 --- a/package.json +++ b/package.json @@ -128,6 +128,8 @@ }, "packageManager": "yarn@1.22.21", "resolutions": { + "**/astro/devalue": "5.6.2", + "**/astro/unstorage/h3": "1.15.5", "**/axios/form-data": "4.0.4", "**/jsdom/form-data": "4.0.4", "**/node-fetch/form-data": "3.0.4", diff --git a/yarn.lock b/yarn.lock index 8530b722f7a..70e75a3f78f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11877,10 +11877,10 @@ deterministic-object-hash@^2.0.2: dependencies: base-64 "^1.0.0" -devalue@^5.5.0: - version "5.6.1" - resolved "https://registry.ag-grid.com/devalue/-/devalue-5.6.1.tgz#f4c0a6e71d1a2bc50c02f9ca3c54ecafeb6a0445" - integrity sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A== +devalue@5.6.2, devalue@^5.5.0: + version "5.6.2" + resolved "https://registry.ag-grid.com/devalue/-/devalue-5.6.2.tgz#931e2bb1cc2b299e0f0fb9e4e5be8ebf521a25b8" + integrity sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg== devlop@^1.0.0, devlop@^1.1.0: version "1.1.0" @@ -14289,19 +14289,19 @@ gzip-size@^6.0.0: dependencies: duplexer "^0.1.2" -h3@^1.15.4: - version "1.15.4" - resolved "http://52.50.158.57:4873/h3/-/h3-1.15.4.tgz#022ab3563bbaf2108c25375c40460f3e54a5fe02" - integrity sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ== +h3@1.15.5, h3@^1.15.4: + version "1.15.5" + resolved "https://registry.ag-grid.com/h3/-/h3-1.15.5.tgz#e2f28d4a66a249973bb050eaddb06b9ab55506f8" + integrity sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg== dependencies: cookie-es "^1.2.2" crossws "^0.3.5" defu "^6.1.4" destr "^2.0.5" iron-webcrypto "^1.2.1" - node-mock-http "^1.0.2" + node-mock-http "^1.0.4" radix3 "^1.1.2" - ufo "^1.6.1" + ufo "^1.6.3" uncrypto "^0.1.3" handle-thing@^2.0.0: @@ -18448,10 +18448,10 @@ node-machine-id@1.1.12: resolved "http://52.50.158.57:4873/node-machine-id/-/node-machine-id-1.1.12.tgz#37904eee1e59b320bb9c5d6c0a59f3b469cb6267" integrity sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ== -node-mock-http@^1.0.2: - version "1.0.3" - resolved "http://52.50.158.57:4873/node-mock-http/-/node-mock-http-1.0.3.tgz#4e55e093267a3b910cded7354389ce2d02c89e77" - integrity sha512-jN8dK25fsfnMrVsEhluUTPkBFY+6ybu7jSB1n+ri/vOGjJxU8J9CZhpSGkHXSkFjtUhbmoncG/YG9ta5Ludqog== +node-mock-http@^1.0.4: + version "1.0.4" + resolved "https://registry.ag-grid.com/node-mock-http/-/node-mock-http-1.0.4.tgz#21f2ab4ce2fe4fbe8a660d7c5195a1db85e042a4" + integrity sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ== node-releases@^2.0.26: version "2.0.27" @@ -24156,6 +24156,11 @@ ufo@^1.6.1: resolved "http://52.50.158.57:4873/ufo/-/ufo-1.6.1.tgz#ac2db1d54614d1b22c1d603e3aef44a85d8f146b" integrity sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA== +ufo@^1.6.3: + version "1.6.3" + resolved "https://registry.ag-grid.com/ufo/-/ufo-1.6.3.tgz#799666e4e88c122a9659805e30b9dc071c3aed4f" + integrity sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q== + uglify-js@^3.1.4: version "3.19.3" resolved "http://52.50.158.57:4873/uglify-js/-/uglify-js-3.19.3.tgz#82315e9bbc6f2b25888858acd1fff8441035b77f" From b2a4da53d795941a12728417fd5d7032ba92b99b Mon Sep 17 00:00:00 2001 From: Alan Treadway Date: Mon, 19 Jan 2026 13:35:18 +0000 Subject: [PATCH 11/12] Integrate sync-rulesync.sh. --- external/ag-shared/scripts/sync-rulesync/sync-rulesync.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/external/ag-shared/scripts/sync-rulesync/sync-rulesync.sh b/external/ag-shared/scripts/sync-rulesync/sync-rulesync.sh index c159c6de919..21763c4bf45 100755 --- a/external/ag-shared/scripts/sync-rulesync/sync-rulesync.sh +++ b/external/ag-shared/scripts/sync-rulesync/sync-rulesync.sh @@ -375,6 +375,12 @@ check_postinstall() { return 0 fi + # Direct invocation: postinstall:patch contains apply-patches.sh + if [[ "$postinstall_script" == *"apply-patches.sh"* ]]; then + log_success "package.json postinstall uses apply-patches.sh" + return 0 + fi + # Indirect via npm-run-all: postinstall runs postinstall:* and postinstall:patch exists if [[ "$postinstall_script" == *"postinstall:*"* ]]; then # Check for direct patch-package in postinstall:patch From 9b0350ed4502d0a14dbdb8db384a5e99c2e78224 Mon Sep 17 00:00:00 2001 From: Alan Treadway Date: Mon, 19 Jan 2026 13:35:38 +0000 Subject: [PATCH 12/12] git subrepo push external/ag-shared subrepo: subdir: "external/ag-shared" merged: "fc38ac74917" upstream: origin: "https://github.com/ag-grid/ag-shared.git" branch: "latest" commit: "fc38ac74917" git-subrepo: version: "0.4.9" origin: "https://github.com/Homebrew/brew" commit: "70badad488" --- external/ag-shared/.gitrepo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/external/ag-shared/.gitrepo b/external/ag-shared/.gitrepo index 7e0980f9988..8daaa462047 100644 --- a/external/ag-shared/.gitrepo +++ b/external/ag-shared/.gitrepo @@ -6,7 +6,7 @@ [subrepo] remote = https://github.com/ag-grid/ag-shared.git branch = latest - commit = 2c81ab2253a675b7e071ffc37b822d08f1329b51 - parent = 4e6337f2694f5d855e4570d697dcc944b6c0d149 + commit = fc38ac749175313716cfbfd346a7df38180efe23 + parent = b2a4da53d795941a12728417fd5d7032ba92b99b method = rebase cmdver = 0.4.9