From fe14b2c78fc9d2e58b6d03ad34b706cb4535eb09 Mon Sep 17 00:00:00 2001 From: Janis Hutz Date: Thu, 23 Apr 2026 10:45:03 +0200 Subject: [PATCH] [Astal] Notifications: Delete not working version --- .../modules/Notification.tsx | 4 +- .../astal/components/notifications/handler.ts | 6 + .../components/notifications/handler.tsx | 299 ------- .../astal/components/notifications/helper.ts | 12 - .../astal/components/notifications/icon.tsx | 10 +- .../astal/components/notifications/main.tsx | 32 + .../components/notifications/notification.tsx | 0 .../notifications/notifications.tsx | 113 --- config/astal/eslint.config.mjs | 744 ++++++++++++++++++ 9 files changed, 790 insertions(+), 430 deletions(-) create mode 100644 config/astal/components/notifications/handler.ts delete mode 100644 config/astal/components/notifications/handler.tsx delete mode 100644 config/astal/components/notifications/helper.ts create mode 100644 config/astal/components/notifications/main.tsx create mode 100644 config/astal/components/notifications/notification.tsx delete mode 100644 config/astal/components/notifications/notifications.tsx create mode 100644 config/astal/eslint.config.mjs diff --git a/config/astal/components/notifications-opt/modules/Notification.tsx b/config/astal/components/notifications-opt/modules/Notification.tsx index b533ea3..43623fd 100644 --- a/config/astal/components/notifications-opt/modules/Notification.tsx +++ b/config/astal/components/notifications-opt/modules/Notification.tsx @@ -2,7 +2,7 @@ import { bind } from "astal"; import { Gtk } from "astal/gtk4"; import Notifd from "gi://AstalNotifd"; import { NotificationIcon } from "./Icon"; -import { createTimeoutManager } from "../../../util/notifd"; +import { createTimeoutManager, time, urgency } from "../../../util/notifd"; export function NotificationWidget({ notification, @@ -81,8 +81,8 @@ export function NotificationWidget({ halign={CENTER} valign={CENTER} vexpand={true} + child={NotificationIcon(notification)!} > - {NotificationIcon(notification)} { + +} diff --git a/config/astal/components/notifications/handler.tsx b/config/astal/components/notifications/handler.tsx deleted file mode 100644 index 8468ae0..0000000 --- a/config/astal/components/notifications/handler.tsx +++ /dev/null @@ -1,299 +0,0 @@ -/* -* dotfiles - handler.ts -* -* Created by Janis Hutz 03/21/2025, Licensed under the GPL V3 License -* https://janishutz.com, development@janishutz.com -* -* -*/ - -import { App, Astal, Gdk } from "astal/gtk4" -import Notifd from "gi://AstalNotifd"; -import Notification from "./notifications"; -import { timeout, Variable } from "astal" - -// ─────────────────────────────────────────────────────────────────── -// Config -// ─────────────────────────────────────────────────────────────────── -const TIMEOUT_DELAY = 5000; -let isRunning = false; -let notificationMenuOpen = false; - -interface NotificationDetails { - notification: Notifd.Notification; - backendID: number; - notifdID: number; -} - - -// ─────────────────────────────────────────────────────────────────── -// ╭───────────────────────────────────────────────╮ -// │ Handler │ -// ╰───────────────────────────────────────────────╯ -// ─────────────────────────────────────────────────────────────────── -let ShownNotifications: Variable = Variable( [] ); -let Notifications: NotificationDetails[] = []; - -const notifd = Notifd.get_default(); -notifd.ignoreTimeout = true; - - -/** - * Delete a notification by its internal ID - * @param index The notifd ID of the notification - */ -const deleteNotification = ( index: number ): void => { - hideNotification( index ); - Notifications.splice( index, 1 ); - if ( Notifications.length === 0 ) { - notificationMenuOpen = false; - } -} - -// ─────────────────────────────────────────────────────────────────── - - -/** - * Delete a notification by notifd id - * @param id The notifd ID of the notification - */ -const deleteNotificationByNotificationID = ( id: number ): void => { - const index = findNotificationByNotificationID( id ); - if ( index > -1 ) { - deleteNotification( index ); - } -} - -// ─────────────────────────────────────────────────────────────────── - - -/** - * Find the internal ID from the notifd id for a notification (helper function) - * @param id The notifd ID of the notification - * @returns The internal ID or -1 if not found - */ -const findNotificationByNotificationID = ( id: number ): number => { - // Find index in Notifications array - for (let index = 0; index < Notifications.length; index++) { - if ( Notifications[ index ].notifdID === id ) { - return index; - } - } - - return -1; -} - -// ─────────────────────────────────────────────────────────────────── - - -/** - * Add a notification to the notification handler - * @param id The notifd ID of the notification - */ -const addNotification = ( id: number ): void => { - const currIndex = Notifications.length; - Notifications.push( { - notifdID: id, - backendID: currIndex, - notification: notifd.get_notification( id ) - } ); - - showNotification( currIndex ); -} - -// ─────────────────────────────────────────────────────────────────── - - -/** - * Start the notifd runner and handle notifications. - */ -const hookToNotificationDaemon = (): void => { - if ( isRunning ) { - printerr( '[ Notifications ] Error: Already running' ); - return; - } - isRunning = true; - - notifd.connect( 'notified', ( _, id ) => { - addNotification( id ); - } ); - - notifd.connect( 'resolved', ( _, id ) => { - deleteNotificationByNotificationID( id ); - } ); -} - -// ─────────────────────────────────────────────────────────────────── - - -/** - * Show a notification. It will stay on screen (regardless of removeAgain passed in), if - * critical urgency - * @param id The internal id (index in Notifications array) - * @param removeAgain = true If to remove the notification from the screen again automatically - */ -const showNotification = ( id: number, removeAgain: boolean = true ) => { - // Add notification to UI for display - const not = [...ShownNotifications.get()].reverse(); - not.push( id ); - ShownNotifications.set( not.reverse() ); - - // Set delay to remove the notification again - if ( removeAgain && Notifications[ id ].notification.get_urgency() !== Notifd.Urgency.CRITICAL ) { - timeout( TIMEOUT_DELAY, () => { - hideNotification( id ); - } ); - } -} - -// ─────────────────────────────────────────────────────────────────── - - -/** - * Stop displaying notification - * @param id The internal id (index in the Notifications array) - */ -const hideNotification = ( id: number ) => { - if ( !notificationMenuOpen ) { - const not = [...ShownNotifications.get()]; - not.splice( not.indexOf( id ), 1 ); - ShownNotifications.set( not ); - } -} - -// ─────────────────────────────────────────────────────────────────── - - -/** - * Open the notification menu. Called by toggleNotificationMenu too - */ -const openNotificationMenu = () => { - // Simply show all notifications - notificationMenuOpen = true; - const not = []; - for (let index = 0; index < Notifications.length; index++) { - not.push( index ); - } - ShownNotifications.set( not.reverse() ); -} - -// ─────────────────────────────────────────────────────────────────── - - -/** - * Close the notification menu. Called by toggleNotificationMenu too - */ -const closeNotificationMenu = () => { - // Hide all notifications - notificationMenuOpen = true; - ShownNotifications.set( [] ); -} - -// ─────────────────────────────────────────────────────────────────── - - -/** - * Toggle the notification menu (i.e. show all notifications) - */ -const toggleNotificationMenu = (): string => { - if ( notificationMenuOpen ) { - closeNotificationMenu(); - return 'Toggle notification menu closed'; - } else { - openNotificationMenu(); - return 'Toggled notification menu open'; - } -} - -// ─────────────────────────────────────────────────────────────────── - - -/** - * Delete all notifications - */ -const clearAllNotifications = () => { - Notifications = []; - ShownNotifications.set( [] ); - // TODO: Hiding for each individual deleteNotification - notificationMenuOpen = false; -} - -// ─────────────────────────────────────────────────────────────────── - - -/** - * Delete the newest notifications - */ -const clearNewestNotifications = () => { - const not = [...ShownNotifications.get()]; - not.splice( 0, 1 ); - ShownNotifications.set( not ); - - Notifications.splice( Notifications.length - 1, 1 ); -} - - - -// ─────────────────────────────────────────────────────────────────── -// ╭───────────────────────────────────────────────╮ -// │ User Interface │ -// ╰───────────────────────────────────────────────╯ -// ─────────────────────────────────────────────────────────────────── -const startNotificationHandler = (gdkmonitor: Gdk.Monitor) => { - const { TOP, RIGHT } = Astal.WindowAnchor - - hookToNotificationDaemon(); - - return list.length > 0 )} - application={App}> - - {ShownNotifications( list => list.map( i => { - // i is index in ShownNotifications array - return - } ) ) } - - -} - -const cliHandler = ( args: string[] ): string => { - if ( args[ 1 ] == 'show' ) { - openNotificationMenu(); - return 'Showing all open notifications'; - } else if ( args[ 1 ] == 'hide' ) { - closeNotificationMenu(); - return 'Hid all notifications'; - } else if ( args[ 1 ] == 'clear' ) { - clearAllNotifications(); - return 'Cleared all notifications'; - } else if ( args[ 1 ] == 'clear-newest' ) { - clearNewestNotifications(); - return 'Cleared newest notification'; - } else if ( args[ 1 ] == 'toggle' ) { - return toggleNotificationMenu(); - } else if ( args[ 1 ] == 'list' ){ - if ( Notifications.length > 0 ) { - let list = 'Currently unviewed notifications: '; - for (let index = 0; index < Notifications.length; index++) { - const element = Notifications[index]; - - list += `\n - (${element.notifdID}) ${element.notification.get_app_name()}: ${element.notification.get_summary()}`; - } - return list; - } else { - return 'No currently unviewed notifications' - } - } else { - return 'Unknown command!'; - } -} - - -export default { - startNotificationHandler, - cliHandler -} diff --git a/config/astal/components/notifications/helper.ts b/config/astal/components/notifications/helper.ts deleted file mode 100644 index 78883f5..0000000 --- a/config/astal/components/notifications/helper.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Gtk, Gdk } from "astal/gtk4"; -import { GLib } from "astal"; - -export const isIcon = (icon: string) => { - const display = Gdk.Display.get_default(); - if (!display) return false; - const iconTheme = Gtk.IconTheme.get_for_display(display); - return iconTheme.has_icon(icon); -}; - -export const fileExists = (path: string) => - GLib.file_test(path, GLib.FileTest.EXISTS); diff --git a/config/astal/components/notifications/icon.tsx b/config/astal/components/notifications/icon.tsx index 8d6b75b..1a0234b 100644 --- a/config/astal/components/notifications/icon.tsx +++ b/config/astal/components/notifications/icon.tsx @@ -8,14 +8,16 @@ export function NotificationIcon(notification: Notifd.Notification) { const icon = notification.image || notification.appIcon || notification.desktopEntry; if (fileExists(icon)) { return ( - - + + }> ); } else if (isIcon(icon)) { return ( - - + + }> ); } diff --git a/config/astal/components/notifications/main.tsx b/config/astal/components/notifications/main.tsx new file mode 100644 index 0000000..2b86cbb --- /dev/null +++ b/config/astal/components/notifications/main.tsx @@ -0,0 +1,32 @@ +import { Astal } from "astal/gtk4"; +import Notifd from "gi://AstalNotifd"; +import Hyprland from "gi://AstalHyprland"; +import { bind } from "astal"; +import { NotificationWidget } from "./modules/Notification"; +import { hyprToGdk } from "../../util/hyprland"; + +export default function Notifications() { + const notifd = Notifd.get_default(); + const hyprland = Hyprland.get_default(); + const { TOP, RIGHT } = Astal.WindowAnchor; + + return ( + hyprToGdk(focused), + )} + anchor={TOP | RIGHT} + visible={bind(notifd, "notifications").as( + (notifications) => notifications.length > 0, + )} + child={ + + {bind(notifd, "notifications").as((notifications) => + notifications.map((n) => ), + )} + + } + /> + ); +} diff --git a/config/astal/components/notifications/notification.tsx b/config/astal/components/notifications/notification.tsx new file mode 100644 index 0000000..e69de29 diff --git a/config/astal/components/notifications/notifications.tsx b/config/astal/components/notifications/notifications.tsx deleted file mode 100644 index c63cb58..0000000 --- a/config/astal/components/notifications/notifications.tsx +++ /dev/null @@ -1,113 +0,0 @@ -// From astal examples - -import { bind, GLib } from "astal"; -import { Gtk } from "astal/gtk4"; -import Notifd from "gi://AstalNotifd"; -import { NotificationIcon } from "./icon"; -// import Pango from "gi://Pango?version=1.0" - -const fileExists = (path: string) => GLib.file_test(path, GLib.FileTest.EXISTS); - -const time = (time: number, format = "%H:%M") => - GLib.DateTime.new_from_unix_local(time).format(format)!; - -const urgency = (n: Notifd.Notification) => { - const { LOW, NORMAL, CRITICAL } = Notifd.Urgency; - // match operator when? - switch (n.urgency) { - case LOW: - return "low"; - case CRITICAL: - return "critical"; - case NORMAL: - default: - return "normal"; - } -}; - -type Props = { - delete: (id: number) => void; - notification: Notifd.Notification; - id: number; -}; - -export default function Notification(props: Props) { - const { notification: n, id: id, delete: del } = props; - const { START, CENTER, END } = Gtk.Align; - - return ( - - - {n.appIcon || n.desktopEntry ? ( - - ) : ( - - )} - - - - - {NotificationIcon(n)} - - - - - {n.get_actions().length > 0 ? ( - - {n.get_actions().map(({ label, id }) => ( - - ))} - - ) : ( - - )} - - ); -} diff --git a/config/astal/eslint.config.mjs b/config/astal/eslint.config.mjs new file mode 100644 index 0000000..48eb7aa --- /dev/null +++ b/config/astal/eslint.config.mjs @@ -0,0 +1,744 @@ +import eslint from '@eslint/js'; +import globals from 'globals'; +import stylistic from '@stylistic/eslint-plugin'; +import tseslint from 'typescript-eslint'; +import typescript from '@typescript-eslint/eslint-plugin'; +import vue from 'eslint-plugin-vue'; + +const style = { + 'plugins': { + '@stylistic': stylistic, + '@stylistic/js': stylistic, + '@stylistic/ts': stylistic + }, + 'files': [ + '**/*.ts', + '**/*.js', + '**/*.mjs', + '**/*.cjs', + '**/*.tsx', + '**/*.jsx' + ], + 'rules': { + 'sort-imports': [ + 'warn', + { + 'ignoreCase': false, + 'ignoreDeclarationSort': false, + 'ignoreMemberSort': false, + 'memberSyntaxSortOrder': [ + 'none', + 'all', + 'multiple', + 'single' + ], + 'allowSeparatedGroups': false + } + ], + // Formatting + '@stylistic/array-bracket-newline': [ + 'error', + { + 'multiline': false, + 'minItems': 2 + } + ], + '@stylistic/array-bracket-spacing': [ + 'error', + 'always' + ], + '@stylistic/array-element-newline': [ + 'error', + { + 'consistent': false, + 'multiline': false, + 'minItems': 2 + } + ], + '@stylistic/arrow-parens': [ + 'error', + 'as-needed' + ], + '@stylistic/arrow-spacing': [ + 'error', + { + 'before': true, + 'after': true + } + ], + '@stylistic/block-spacing': [ + 'error', + 'always' + ], + '@stylistic/brace-style': [ + 'error', + '1tbs', + { + 'allowSingleLine': false + } + ], + '@stylistic/comma-dangle': [ + 'error', + 'never' + ], + '@stylistic/comma-spacing': [ + 'error', + { + 'before': false, + 'after': true + } + ], + '@stylistic/comma-style': [ + 'error', + 'last' + ], + '@stylistic/dot-location': [ + 'error', + 'property' + ], + '@stylistic/function-call-argument-newline': [ + 'error', + 'consistent' + ], + '@stylistic/function-call-spacing': [ + 'error', + 'never' + ], + '@stylistic/function-paren-newline': [ + 'error', + 'multiline-arguments' + ], + '@stylistic/implicit-arrow-linebreak': [ + 'error', + 'beside' + ], + '@stylistic/indent': [ + 'error', + 4 + ], + '@stylistic/indent-binary-ops': [ + 'error', + 4 + ], + '@stylistic/key-spacing': [ + 'error', + { + 'beforeColon': false, + 'afterColon': true + } + ], + '@stylistic/keyword-spacing': [ + 'error', + { + 'before': true, + 'after': true + } + ], + '@stylistic/lines-between-class-members': [ + 'error', + 'always' + ], + '@stylistic/max-len': [ + 'warn', + { + 'code': 140, + 'comments': 160, + 'ignoreComments': false, + 'ignoreUrls': true, + 'ignoreStrings': true, + 'ignoreTemplateLiterals': true, + 'ignoreRegExpLiterals': true + } + ], + '@stylistic/max-statements-per-line': [ + 'error', + { + 'max': 1 + } + ], + '@stylistic/multiline-ternary': [ + 'error', + 'always-multiline' + ], + '@stylistic/new-parens': [ + 'error', + 'always' + ], + '@stylistic/newline-per-chained-call': 'error', + '@stylistic/no-confusing-arrow': 'error', + '@stylistic/no-extra-parens': [ + 'error', + 'all', + { + 'nestedBinaryExpressions': false, + 'ternaryOperandBinaryExpressions': false, + 'ignoreJSX': 'multi-line', + 'nestedConditionalExpressions': false + } + ], + '@stylistic/no-extra-semi': 'error', + '@stylistic/no-floating-decimal': 'error', + '@stylistic/no-mixed-operators': 'error', + '@stylistic/no-mixed-spaces-and-tabs': 'error', + '@stylistic/no-multi-spaces': 'error', + '@stylistic/no-multiple-empty-lines': [ + 'error', + { + 'max': 3, + 'maxEOF': 2 + } + ], + '@stylistic/no-tabs': 'error', + '@stylistic/no-trailing-spaces': 'error', + '@stylistic/no-whitespace-before-property': 'error', + '@stylistic/object-curly-newline': [ + 'error', + { + 'multiline': true, + 'minProperties': 1 + } + ], + '@stylistic/object-curly-spacing': [ + 'error', + 'always' + ], + '@stylistic/object-property-newline': 'error', + '@stylistic/one-var-declaration-per-line': 'error', + '@stylistic/operator-linebreak': [ + 'error', + 'before' + ], + '@stylistic/padded-blocks': [ + 'error', + { + 'blocks': 'never', + 'classes': 'always', + 'switches': 'never' + } + ], + + // Padding lines. The most in-depth part of this config + '@stylistic/padding-line-between-statements': [ + 'error', + // Variables, Constants + { + 'blankLine': 'never', + 'prev': 'var', + 'next': 'var' + }, + { + 'blankLine': 'never', + 'prev': 'let', + 'next': 'let' + }, + { + 'blankLine': 'never', + 'prev': 'const', + 'next': 'const' + }, + { + 'blankLine': 'always', + 'prev': 'var', + 'next': [ + 'block', + 'block-like', + 'break', + 'cjs-export', + 'cjs-import', + 'class', + 'const', + 'continue', + 'debugger', + 'directive', + 'do', + 'empty', + 'export', + 'expression', + 'for', + 'function', + 'if', + 'iife', + 'import', + 'let', + 'return', + 'switch', + 'throw', + 'try', + 'var', + 'with' + ] + }, + { + 'blankLine': 'always', + 'prev': 'let', + 'next': [ + 'block', + 'block-like', + 'break', + 'cjs-export', + 'cjs-import', + 'class', + 'const', + 'continue', + 'debugger', + 'directive', + 'do', + 'empty', + 'export', + 'expression', + 'for', + 'function', + 'if', + 'iife', + 'import', + 'return', + 'switch', + 'throw', + 'try', + 'var', + 'while', + 'with' + ] + }, + { + 'blankLine': 'always', + 'prev': 'const', + 'next': [ + 'block', + 'block-like', + 'break', + 'cjs-export', + 'cjs-import', + 'class', + 'continue', + 'debugger', + 'directive', + 'do', + 'empty', + 'export', + 'expression', + 'for', + 'function', + 'if', + 'iife', + 'import', + 'let', + 'return', + 'switch', + 'throw', + 'try', + 'var', + 'while', + 'with' + ] + }, + // Import + { + 'blankLine': 'never', + 'prev': 'import', + 'next': 'import' + }, + { + 'blankLine': 'never', + 'prev': 'cjs-import', + 'next': 'cjs-import' + }, + { + 'blankLine': 'always', + 'prev': [ + 'block', + 'block-like', + 'break', + 'cjs-export', + 'class', + 'const', + 'continue', + 'debugger', + 'directive', + 'do', + 'empty', + 'export', + 'expression', + 'for', + 'function', + 'if', + 'iife', + 'let', + 'return', + 'switch', + 'throw', + 'try', + 'var', + 'while', + 'with' + ], + 'next': 'cjs-import' + }, + { + 'blankLine': 'always', + 'prev': 'cjs-import', + 'next': [ + 'block', + 'block-like', + 'break', + 'cjs-export', + 'class', + 'const', + 'continue', + 'debugger', + 'directive', + 'do', + 'empty', + 'export', + 'expression', + 'for', + 'function', + 'if', + 'iife', + 'let', + 'return', + 'switch', + 'throw', + 'try', + 'var', + 'while', + 'with' + ] + }, + { + 'blankLine': 'always', + 'prev': [ + 'block', + 'block-like', + 'break', + 'cjs-export', + 'class', + 'const', + 'continue', + 'debugger', + 'directive', + 'do', + 'empty', + 'export', + 'expression', + 'for', + 'function', + 'if', + 'iife', + 'let', + 'return', + 'switch', + 'throw', + 'try', + 'var', + 'while', + 'with' + ], + 'next': 'import' + }, + { + 'blankLine': 'always', + 'prev': 'import', + 'next': [ + 'block', + 'block-like', + 'break', + 'cjs-export', + 'class', + 'const', + 'continue', + 'debugger', + 'directive', + 'do', + 'empty', + 'export', + 'expression', + 'for', + 'function', + 'if', + 'iife', + 'let', + 'return', + 'switch', + 'throw', + 'try', + 'var', + 'while', + 'with' + ] + }, + // If + { + 'blankLine': 'always', + 'prev': '*', + 'next': 'if' + }, + { + 'blankLine': 'always', + 'prev': 'if', + 'next': '*' + }, + // For + { + 'blankLine': 'always', + 'prev': '*', + 'next': 'for' + }, + { + 'blankLine': 'always', + 'prev': 'for', + 'next': '*' + }, + // While + { + 'blankLine': 'always', + 'prev': '*', + 'next': 'while' + }, + { + 'blankLine': 'always', + 'prev': 'while', + 'next': '*' + }, + // Functions + { + 'blankLine': 'always', + 'prev': '*', + 'next': 'function' + }, + { + 'blankLine': 'always', + 'prev': 'function', + 'next': '*' + }, + // Block Statements + { + 'blankLine': 'always', + 'prev': '*', + 'next': 'block-like' + }, + { + 'blankLine': 'always', + 'prev': 'block-like', + 'next': '*' + }, + // Switch + { + 'blankLine': 'always', + 'prev': '*', + 'next': 'switch' + }, + { + 'blankLine': 'always', + 'prev': 'switch', + 'next': '*' + }, + // Try-Catch + { + 'blankLine': 'always', + 'prev': '*', + 'next': 'try' + }, + { + 'blankLine': 'always', + 'prev': 'try', + 'next': '*' + }, + // Throw + { + 'blankLine': 'always', + 'prev': '*', + 'next': 'throw' + }, + { + 'blankLine': 'always', + 'prev': 'throw', + 'next': '*' + }, + // Return + { + 'blankLine': 'never', + 'prev': 'return', + 'next': '*' + }, + { + 'blankLine': 'always', + 'prev': '*', + 'next': 'return' + }, + // Export + { + 'blankLine': 'always', + 'prev': '*', + 'next': 'export' + }, + { + 'blankLine': 'always', + 'prev': 'export', + 'next': '*' + }, + { + 'blankLine': 'always', + 'prev': '*', + 'next': 'cjs-export' + }, + { + 'blankLine': 'always', + 'prev': 'cjs-export', + 'next': '*' + }, + // Classes + { + 'blankLine': 'always', + 'prev': '*', + 'next': 'class' + }, + { + 'blankLine': 'always', + 'prev': 'class', + 'next': '*' + } + ], + '@stylistic/quote-props': [ + 'error', + 'always' + ], + '@stylistic/quotes': [ + 'error', + 'single' + ], + '@stylistic/rest-spread-spacing': [ + 'error', + 'never' + ], + '@stylistic/semi': [ + 'error', + 'always' + ], + '@stylistic/semi-spacing': [ + 'error', + { + 'before': false, + 'after': true + } + ], + '@stylistic/semi-style': [ + 'error', + 'last' + ], + '@stylistic/space-before-blocks': [ + 'error', + 'always' + ], + '@stylistic/space-before-function-paren': [ + 'error', + 'always' + ], + '@stylistic/space-in-parens': [ + 'error', + 'always' + ], + '@stylistic/space-infix-ops': [ + 'error', + { + 'int32Hint': false + } + ], + '@stylistic/space-unary-ops': 'error', + '@stylistic/spaced-comment': [ + 'error', + 'always' + ], + '@stylistic/switch-colon-spacing': 'error', + '@stylistic/template-curly-spacing': [ + 'error', + 'always' + ], + '@stylistic/template-tag-spacing': [ + 'error', + 'always' + ], + '@stylistic/type-generic-spacing': 'error', + '@stylistic/type-named-tuple-spacing': 'error', + '@stylistic/wrap-iife': [ + 'error', + 'inside' + ], + '@stylistic/wrap-regex': 'error', + '@stylistic/ts/type-annotation-spacing': 'error' + } +}; + +/** @type {import('eslint').Linter.Config} */ +export default tseslint.config( + // Base JavaScript rules + eslint.configs.recommended, + tseslint.configs.recommended, + style, + + // Vue support (including TS and JSX inside SFCs) + { + 'files': [ '**/*.vue' ], + 'languageOptions': { + 'sourceType': 'module', + 'ecmaVersion': 'latest', + 'globals': globals.browser, + 'parserOptions': { + 'parser': tseslint.parser + } + }, + 'plugins': { + 'vue': vue, + '@stylistic': stylistic, + '@stylistic/js': stylistic, + '@stylistic/ts': stylistic, + '@typescript-eslint': typescript + }, + 'extends': [ + eslint.configs.recommended, + ...vue.configs['flat/recommended'] + ], + 'rules': { + ...typescript.configs.recommended.rules, + ...style.rules, + + // Vue specific rules + '@stylistic/indent': 'off', + 'vue/html-indent': [ + 'error', + 4 + ], + 'vue/html-comment-indent': [ + 'error', + 4 + ], + 'vue/script-indent': [ + 'error', + 4, + { + 'baseIndent': 1, + 'switchCase': 1 + } + ], + 'vue/html-self-closing': [ + 'error', + { + 'html': { + 'void': 'never', + 'normal': 'never', + 'component': 'always' + }, + 'svg': 'always', + 'math': 'never' + } + ], + 'vue/max-attributes-per-line': [ + 'error', + { + 'singleline': 3, + 'multiline': 1 + } + ] + } + } +);