diff --git a/fakemui/fakemui/feedback/Skeleton.tsx b/fakemui/fakemui/feedback/Skeleton.tsx index feb37a663..86edd6cd7 100644 --- a/fakemui/fakemui/feedback/Skeleton.tsx +++ b/fakemui/fakemui/feedback/Skeleton.tsx @@ -1,13 +1,66 @@ import React from 'react' +import { classNames } from '../utils/classNames' -export type SkeletonVariant = 'text' | 'rectangular' | 'circular' +export type SkeletonVariant = 'text' | 'rectangular' | 'circular' | 'rounded' +export type SkeletonAnimation = 'pulse' | 'wave' | false -export interface SkeletonProps extends React.HTMLAttributes { +export interface SkeletonProps extends React.HTMLAttributes { + /** The type of skeleton shape */ variant?: SkeletonVariant + /** Width of the skeleton (accepts CSS units) */ width?: string | number + /** Height of the skeleton (accepts CSS units) */ height?: string | number + /** Animation type or false to disable */ + animation?: SkeletonAnimation + /** If true, skeleton takes up full width of parent */ + fullWidth?: boolean + /** Custom component to render as skeleton root */ + component?: React.ElementType } -export const Skeleton: React.FC = ({ variant = 'text', width, height, className = '', ...props }) => ( -
-) +/** + * Loading placeholder with shimmer animation + * + * @example + * ```tsx + * // Basic text skeleton + * + * + * // Avatar placeholder + * + * + * // Card image placeholder + * + * ``` + */ +export const Skeleton: React.FC = ({ + variant = 'text', + width, + height, + animation = 'pulse', + fullWidth = false, + component: Component = 'span', + className, + style, + ...props +}) => { + const rootClassName = classNames( + 'fakemui-skeleton', + `fakemui-skeleton--${variant}`, + animation && `fakemui-skeleton--${animation}`, + fullWidth && 'fakemui-skeleton--full-width', + className + ) + + const rootStyle: React.CSSProperties = { + ...style, + width: fullWidth ? '100%' : width, + height, + } + + return +} + +export default Skeleton + diff --git a/fakemui/icons/ArrowLeft.tsx b/fakemui/icons/ArrowLeft.tsx new file mode 100644 index 000000000..9e56ae634 --- /dev/null +++ b/fakemui/icons/ArrowLeft.tsx @@ -0,0 +1,9 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const ArrowLeft = (props: IconProps) => ( + + + + +) diff --git a/fakemui/icons/ArrowRight.tsx b/fakemui/icons/ArrowRight.tsx new file mode 100644 index 000000000..11fec5734 --- /dev/null +++ b/fakemui/icons/ArrowRight.tsx @@ -0,0 +1,9 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const ArrowRight = (props: IconProps) => ( + + + + +) diff --git a/fakemui/icons/BookOpen.tsx b/fakemui/icons/BookOpen.tsx new file mode 100644 index 000000000..ee3cdd0fd --- /dev/null +++ b/fakemui/icons/BookOpen.tsx @@ -0,0 +1,9 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const BookOpen = (props: IconProps) => ( + + + + +) diff --git a/fakemui/icons/Broom.tsx b/fakemui/icons/Broom.tsx new file mode 100644 index 000000000..2704367eb --- /dev/null +++ b/fakemui/icons/Broom.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const Broom = (props: IconProps) => ( + + + + + +) diff --git a/fakemui/icons/Buildings.tsx b/fakemui/icons/Buildings.tsx new file mode 100644 index 000000000..319d79cc2 --- /dev/null +++ b/fakemui/icons/Buildings.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const Buildings = (props: IconProps) => ( + + + + + + + + + + +) diff --git a/fakemui/icons/CaretDown.tsx b/fakemui/icons/CaretDown.tsx new file mode 100644 index 000000000..c6cdcdc08 --- /dev/null +++ b/fakemui/icons/CaretDown.tsx @@ -0,0 +1,8 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const CaretDown = (props: IconProps) => ( + + + +) diff --git a/fakemui/icons/CaretRight.tsx b/fakemui/icons/CaretRight.tsx new file mode 100644 index 000000000..f18cb3a2a --- /dev/null +++ b/fakemui/icons/CaretRight.tsx @@ -0,0 +1,8 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const CaretRight = (props: IconProps) => ( + + + +) diff --git a/fakemui/icons/ChatCircle.tsx b/fakemui/icons/ChatCircle.tsx new file mode 100644 index 000000000..9bc2a6eaa --- /dev/null +++ b/fakemui/icons/ChatCircle.tsx @@ -0,0 +1,8 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const ChatCircle = (props: IconProps) => ( + + + +) diff --git a/fakemui/icons/CheckCircle.tsx b/fakemui/icons/CheckCircle.tsx new file mode 100644 index 000000000..380a13e3e --- /dev/null +++ b/fakemui/icons/CheckCircle.tsx @@ -0,0 +1,9 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const CheckCircle = (props: IconProps) => ( + + + + +) diff --git a/fakemui/icons/ColumnResize.tsx b/fakemui/icons/ColumnResize.tsx new file mode 100644 index 000000000..47a383e62 --- /dev/null +++ b/fakemui/icons/ColumnResize.tsx @@ -0,0 +1,13 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const ColumnResize = (props: IconProps) => ( + + + + + + +) + +export default ColumnResize diff --git a/fakemui/icons/Crown.tsx b/fakemui/icons/Crown.tsx new file mode 100644 index 000000000..be430ee4f --- /dev/null +++ b/fakemui/icons/Crown.tsx @@ -0,0 +1,8 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const Crown = (props: IconProps) => ( + + + +) diff --git a/fakemui/icons/Csv.tsx b/fakemui/icons/Csv.tsx new file mode 100644 index 000000000..581792d7a --- /dev/null +++ b/fakemui/icons/Csv.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const Csv = (props: IconProps) => ( + + + + CSV + +) + +export default Csv diff --git a/fakemui/icons/Envelope.tsx b/fakemui/icons/Envelope.tsx new file mode 100644 index 000000000..517ec34a8 --- /dev/null +++ b/fakemui/icons/Envelope.tsx @@ -0,0 +1,9 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const Envelope = (props: IconProps) => ( + + + + +) diff --git a/fakemui/icons/Export.tsx b/fakemui/icons/Export.tsx new file mode 100644 index 000000000..ddf7e5348 --- /dev/null +++ b/fakemui/icons/Export.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const Export = (props: IconProps) => ( + + + + + +) diff --git a/fakemui/icons/Funnel.tsx b/fakemui/icons/Funnel.tsx new file mode 100644 index 000000000..e07850d51 --- /dev/null +++ b/fakemui/icons/Funnel.tsx @@ -0,0 +1,8 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const Funnel = (props: IconProps) => ( + + + +) diff --git a/fakemui/icons/Gear.tsx b/fakemui/icons/Gear.tsx new file mode 100644 index 000000000..fe3a0b4b0 --- /dev/null +++ b/fakemui/icons/Gear.tsx @@ -0,0 +1,9 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const Gear = (props: IconProps) => ( + + + + +) diff --git a/fakemui/icons/GithubLogo.tsx b/fakemui/icons/GithubLogo.tsx new file mode 100644 index 000000000..d5df65c18 --- /dev/null +++ b/fakemui/icons/GithubLogo.tsx @@ -0,0 +1,8 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const GithubLogo = (props: IconProps) => ( + + + +) diff --git a/fakemui/icons/House.tsx b/fakemui/icons/House.tsx new file mode 100644 index 000000000..9583d9774 --- /dev/null +++ b/fakemui/icons/House.tsx @@ -0,0 +1,8 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const House = (props: IconProps) => ( + + + +) diff --git a/fakemui/icons/MagnifyingGlass.tsx b/fakemui/icons/MagnifyingGlass.tsx new file mode 100644 index 000000000..2ae0210ae --- /dev/null +++ b/fakemui/icons/MagnifyingGlass.tsx @@ -0,0 +1,9 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const MagnifyingGlass = (props: IconProps) => ( + + + + +) diff --git a/fakemui/icons/Palette.tsx b/fakemui/icons/Palette.tsx new file mode 100644 index 000000000..bda8b84fc --- /dev/null +++ b/fakemui/icons/Palette.tsx @@ -0,0 +1,13 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const Palette = (props: IconProps) => ( + + + + + + + + +) diff --git a/fakemui/icons/Power.tsx b/fakemui/icons/Power.tsx new file mode 100644 index 000000000..cfff39abf --- /dev/null +++ b/fakemui/icons/Power.tsx @@ -0,0 +1,9 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const Power = (props: IconProps) => ( + + + + +) diff --git a/fakemui/icons/RowSelect.tsx b/fakemui/icons/RowSelect.tsx new file mode 100644 index 000000000..a456b5716 --- /dev/null +++ b/fakemui/icons/RowSelect.tsx @@ -0,0 +1,14 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const RowSelect = (props: IconProps) => ( + + + + + + + +) + +export default RowSelect diff --git a/fakemui/icons/SelectAll.tsx b/fakemui/icons/SelectAll.tsx new file mode 100644 index 000000000..83a6e630a --- /dev/null +++ b/fakemui/icons/SelectAll.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const SelectAll = (props: IconProps) => ( + + + + + + + + +) + +export default SelectAll diff --git a/fakemui/icons/SignIn.tsx b/fakemui/icons/SignIn.tsx new file mode 100644 index 000000000..f56f403e6 --- /dev/null +++ b/fakemui/icons/SignIn.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const SignIn = (props: IconProps) => ( + + + + + +) diff --git a/fakemui/icons/SignOut.tsx b/fakemui/icons/SignOut.tsx new file mode 100644 index 000000000..d2991c7ca --- /dev/null +++ b/fakemui/icons/SignOut.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const SignOut = (props: IconProps) => ( + + + + + +) diff --git a/fakemui/icons/SortAscending.tsx b/fakemui/icons/SortAscending.tsx new file mode 100644 index 000000000..7125b701f --- /dev/null +++ b/fakemui/icons/SortAscending.tsx @@ -0,0 +1,14 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const SortAscending = (props: IconProps) => ( + + + + + + + +) + +export default SortAscending diff --git a/fakemui/icons/SortDescending.tsx b/fakemui/icons/SortDescending.tsx new file mode 100644 index 000000000..11a19093c --- /dev/null +++ b/fakemui/icons/SortDescending.tsx @@ -0,0 +1,14 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const SortDescending = (props: IconProps) => ( + + + + + + + +) + +export default SortDescending diff --git a/fakemui/icons/Sparkle.tsx b/fakemui/icons/Sparkle.tsx new file mode 100644 index 000000000..6d4ad6c16 --- /dev/null +++ b/fakemui/icons/Sparkle.tsx @@ -0,0 +1,9 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const Sparkle = (props: IconProps) => ( + + + + +) diff --git a/fakemui/icons/SquaresFour.tsx b/fakemui/icons/SquaresFour.tsx new file mode 100644 index 000000000..da6efa2b1 --- /dev/null +++ b/fakemui/icons/SquaresFour.tsx @@ -0,0 +1,11 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const SquaresFour = (props: IconProps) => ( + + + + + + +) diff --git a/fakemui/icons/Table.tsx b/fakemui/icons/Table.tsx new file mode 100644 index 000000000..4fb683dee --- /dev/null +++ b/fakemui/icons/Table.tsx @@ -0,0 +1,11 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const Table = (props: IconProps) => ( + + + + + + +) diff --git a/fakemui/icons/TableCells.tsx b/fakemui/icons/TableCells.tsx new file mode 100644 index 000000000..e88a11d32 --- /dev/null +++ b/fakemui/icons/TableCells.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const TableCells = (props: IconProps) => ( + + + + + + + + +) + +export default TableCells diff --git a/fakemui/icons/Tree.tsx b/fakemui/icons/Tree.tsx new file mode 100644 index 000000000..f11da2adf --- /dev/null +++ b/fakemui/icons/Tree.tsx @@ -0,0 +1,9 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const Tree = (props: IconProps) => ( + + + + +) diff --git a/fakemui/icons/TrendUp.tsx b/fakemui/icons/TrendUp.tsx new file mode 100644 index 000000000..cb5383115 --- /dev/null +++ b/fakemui/icons/TrendUp.tsx @@ -0,0 +1,9 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const TrendUp = (props: IconProps) => ( + + + + +) diff --git a/fakemui/icons/UserCircle.tsx b/fakemui/icons/UserCircle.tsx new file mode 100644 index 000000000..2649e5593 --- /dev/null +++ b/fakemui/icons/UserCircle.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const UserCircle = (props: IconProps) => ( + + + + + +) diff --git a/fakemui/icons/UserPlus.tsx b/fakemui/icons/UserPlus.tsx new file mode 100644 index 000000000..38000b123 --- /dev/null +++ b/fakemui/icons/UserPlus.tsx @@ -0,0 +1,11 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const UserPlus = (props: IconProps) => ( + + + + + + +) diff --git a/fakemui/icons/Users.tsx b/fakemui/icons/Users.tsx new file mode 100644 index 000000000..89915f1c1 --- /dev/null +++ b/fakemui/icons/Users.tsx @@ -0,0 +1,11 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const Users = (props: IconProps) => ( + + + + + + +) diff --git a/fakemui/icons/XCircle.tsx b/fakemui/icons/XCircle.tsx new file mode 100644 index 000000000..f0c5a8308 --- /dev/null +++ b/fakemui/icons/XCircle.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const XCircle = (props: IconProps) => ( + + + + + +) diff --git a/fakemui/styles/components/Skeleton.module.scss b/fakemui/styles/components/Skeleton.module.scss new file mode 100644 index 000000000..39f64a08e --- /dev/null +++ b/fakemui/styles/components/Skeleton.module.scss @@ -0,0 +1,95 @@ +// Skeleton component styles +// Loading placeholder with shimmer animation + +@keyframes skeleton-shimmer { + 0% { + background-position: -200px 0; + } + 100% { + background-position: calc(200px + 100%) 0; + } +} + +.fakemui-skeleton { + display: inline-block; + background-color: var(--color-secondary, rgba(0, 0, 0, 0.11)); + background-image: linear-gradient( + 90deg, + transparent 0%, + var(--skeleton-shimmer-color, rgba(255, 255, 255, 0.5)) 50%, + transparent 100% + ); + background-size: 200px 100%; + background-repeat: no-repeat; + animation: skeleton-shimmer 1.5s ease-in-out infinite; + + // Variants + &--text { + height: 1em; + border-radius: var(--radius-xs, 4px); + transform: scale(1, 0.6); + transform-origin: 0 60%; + + &:empty::before { + content: '\00a0'; // Non-breaking space for height + } + } + + &--rectangular { + border-radius: var(--radius-sm, 4px); + } + + &--circular { + border-radius: 50%; + } + + &--rounded { + border-radius: var(--radius-md, 8px); + } + + // Wave animation variant + &--wave { + animation: none; + overflow: hidden; + position: relative; + + &::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient( + 90deg, + transparent, + var(--skeleton-shimmer-color, rgba(255, 255, 255, 0.5)), + transparent + ); + animation: skeleton-shimmer 1.6s linear 0.5s infinite; + } + } + + // Pulse animation variant + &--pulse { + animation: skeleton-pulse 1.5s ease-in-out 0.5s infinite; + } +} + +@keyframes skeleton-pulse { + 0% { + opacity: 1; + } + 50% { + opacity: 0.4; + } + 100% { + opacity: 1; + } +} + +// Dark theme adjustments +[data-theme="dark"] .fakemui-skeleton { + background-color: rgba(255, 255, 255, 0.11); + --skeleton-shimmer-color: rgba(255, 255, 255, 0.2); +} diff --git a/packages/admin_dialog/seed/scripts/settings.lua b/packages/admin_dialog/seed/scripts/settings.lua index a99679e26..706168e20 100644 --- a/packages/admin_dialog/seed/scripts/settings.lua +++ b/packages/admin_dialog/seed/scripts/settings.lua @@ -1,44 +1,5 @@ -- Admin settings dialog +-- DEPRECATED: This file redirects to settings/ directory +-- Functions are now split into single-function files ----@class SettingsDialog -local M = {} - ----@class UIComponent ----@field type string ----@field props? table ----@field children? table - ----@return UIComponent -function M.render_general() - return { - type = "dialog", - props = { - title = "General Settings", - size = "large" - }, - children = { - { type = "text_field", props = { label = "Site Name", name = "site_name" } }, - { type = "text_field", props = { label = "Admin Email", name = "admin_email", type = "email" } }, - { type = "switch", props = { label = "Maintenance Mode", name = "maintenance" } }, - { type = "switch", props = { label = "Allow Registration", name = "allow_registration" } } - } - } -end - ----@return UIComponent -function M.render_security() - return { - type = "dialog", - props = { - title = "Security Settings", - size = "medium" - }, - children = { - { type = "number_field", props = { label = "Session Timeout (min)", name = "session_timeout", min = 5 } }, - { type = "number_field", props = { label = "Max Login Attempts", name = "max_attempts", min = 1 } }, - { type = "switch", props = { label = "Require 2FA", name = "require_2fa" } } - } - } -end - -return M +return require("settings.init") diff --git a/packages/admin_dialog/seed/scripts/user.lua b/packages/admin_dialog/seed/scripts/user.lua index 0ad06c2a9..f592e9a7b 100644 --- a/packages/admin_dialog/seed/scripts/user.lua +++ b/packages/admin_dialog/seed/scripts/user.lua @@ -1,52 +1,5 @@ -- User management dialog +-- DEPRECATED: This file redirects to user/ directory +-- Functions are now split into single-function files ----@class UserDialog -local M = {} - ----@class User ----@field username string ----@field email string ----@field role string ----@field active boolean - ----@class UIComponent ----@field type string ----@field props? table ----@field children? table - ----@return UIComponent -function M.render_create() - return { - type = "dialog", - props = { - title = "Create User", - size = "medium" - }, - children = { - { type = "text_field", props = { label = "Username", name = "username", required = true } }, - { type = "text_field", props = { label = "Email", name = "email", type = "email", required = true } }, - { type = "password_field", props = { label = "Password", name = "password", required = true } }, - { type = "select", props = { label = "Role", name = "role", options = {"user", "admin"} } } - } - } -end - ----@param user User ----@return UIComponent -function M.render_edit(user) - return { - type = "dialog", - props = { - title = "Edit User", - size = "medium" - }, - children = { - { type = "text_field", props = { label = "Username", name = "username", value = user.username } }, - { type = "text_field", props = { label = "Email", name = "email", value = user.email } }, - { type = "select", props = { label = "Role", name = "role", value = user.role, options = {"user", "admin"} } }, - { type = "checkbox", props = { label = "Active", name = "active", checked = user.active } } - } - } -end - -return M +return require("user.init") diff --git a/packages/admin_dialog/seed/scripts/user/init.lua b/packages/admin_dialog/seed/scripts/user/init.lua new file mode 100644 index 000000000..9e06b8d49 --- /dev/null +++ b/packages/admin_dialog/seed/scripts/user/init.lua @@ -0,0 +1,16 @@ +-- User dialog module facade +-- Re-exports all user dialog functions for backward compatibility +-- Each function is defined in its own file following 1-function-per-file pattern + +---@class UserDialog +local M = {} + +-- Import all single-function modules +local renderCreate = require("user.render_create") +local renderEdit = require("user.render_edit") + +-- Re-export all functions +M.render_create = renderCreate.render_create +M.render_edit = renderEdit.render_edit + +return M diff --git a/packages/admin_dialog/seed/scripts/user/render_create.lua b/packages/admin_dialog/seed/scripts/user/render_create.lua new file mode 100644 index 000000000..1a3121fc2 --- /dev/null +++ b/packages/admin_dialog/seed/scripts/user/render_create.lua @@ -0,0 +1,25 @@ +-- User create dialog +-- Single function module for admin user dialogs + +---@class RenderCreate +local M = {} + +---Render create user dialog +---@return UIComponent +function M.render_create() + return { + type = "dialog", + props = { + title = "Create User", + size = "medium" + }, + children = { + { type = "text_field", props = { label = "Username", name = "username", required = true } }, + { type = "text_field", props = { label = "Email", name = "email", type = "email", required = true } }, + { type = "password_field", props = { label = "Password", name = "password", required = true } }, + { type = "select", props = { label = "Role", name = "role", options = {"user", "admin"} } } + } + } +end + +return M diff --git a/packages/admin_dialog/seed/scripts/user/render_edit.lua b/packages/admin_dialog/seed/scripts/user/render_edit.lua new file mode 100644 index 000000000..53a520799 --- /dev/null +++ b/packages/admin_dialog/seed/scripts/user/render_edit.lua @@ -0,0 +1,26 @@ +-- User edit dialog +-- Single function module for admin user dialogs + +---@class RenderEdit +local M = {} + +---Render edit user dialog +---@param user User User data to edit +---@return UIComponent +function M.render_edit(user) + return { + type = "dialog", + props = { + title = "Edit User", + size = "medium" + }, + children = { + { type = "text_field", props = { label = "Username", name = "username", value = user.username } }, + { type = "text_field", props = { label = "Email", name = "email", value = user.email } }, + { type = "select", props = { label = "Role", name = "role", value = user.role, options = {"user", "admin"} } }, + { type = "checkbox", props = { label = "Active", name = "active", checked = user.active } } + } + } +end + +return M diff --git a/packages/admin_dialog/seed/scripts/user/types.lua b/packages/admin_dialog/seed/scripts/user/types.lua new file mode 100644 index 000000000..996c1b0a0 --- /dev/null +++ b/packages/admin_dialog/seed/scripts/user/types.lua @@ -0,0 +1,15 @@ +-- Type definitions for user dialog module +-- Shared across all user dialog functions + +---@class User +---@field username string +---@field email string +---@field role string +---@field active boolean + +---@class UIComponent +---@field type string +---@field props? table +---@field children? table + +return {} diff --git a/packages/data_table/seed/scripts/export.lua b/packages/data_table/seed/scripts/export.lua index df2352716..0a1beb9bb 100644 --- a/packages/data_table/seed/scripts/export.lua +++ b/packages/data_table/seed/scripts/export.lua @@ -1,235 +1,5 @@ -- Export utilities for data tables --- Provides CSV and JSON export functionality +-- DEPRECATED: This file redirects to export/ directory +-- Functions are now split into single-function files ----@class Export -local M = {} - ----@class ExportOptions ----@field includeHeaders boolean Include column headers in export ----@field columns string[]|nil Specific columns to export (nil = all) ----@field delimiter string CSV delimiter (default: ",") ----@field lineEnding string Line ending (default: "\n") - ----Escape a value for CSV ----@param value any Value to escape ----@return string Escaped CSV string -function M.escapeCsv(value) - if value == nil then - return "" - end - - local str = tostring(value) - - -- Check if escaping is needed - if string.find(str, '[,"\r\n]') then - -- Escape double quotes by doubling them - str = string.gsub(str, '"', '""') - -- Wrap in double quotes - str = '"' .. str .. '"' - end - - return str -end - ----Get column values from row data ----@param row table Row data object ----@param columns table[] Column definitions ----@param columnIds string[]|nil Specific column IDs (nil = all) ----@return string[] Array of values -function M.getRowValues(row, columns, columnIds) - local values = {} - - if columnIds then - -- Specific columns only - for _, id in ipairs(columnIds) do - table.insert(values, row[id]) - end - else - -- All columns - for _, col in ipairs(columns) do - if col.type ~= "actions" then - table.insert(values, row[col.id]) - end - end - end - - return values -end - ----Get column labels for headers ----@param columns table[] Column definitions ----@param columnIds string[]|nil Specific column IDs (nil = all) ----@return string[] Array of labels -function M.getColumnLabels(columns, columnIds) - local labels = {} - - if columnIds then - -- Specific columns only - for _, id in ipairs(columnIds) do - for _, col in ipairs(columns) do - if col.id == id then - table.insert(labels, col.label or col.id) - break - end - end - end - else - -- All columns (except actions) - for _, col in ipairs(columns) do - if col.type ~= "actions" then - table.insert(labels, col.label or col.id) - end - end - end - - return labels -end - ----Export data to CSV format ----@param data table[] Array of row data objects ----@param columns table[] Column definitions ----@param options? ExportOptions Export options ----@return string CSV formatted string -function M.exportToCsv(data, columns, options) - options = options or {} - local includeHeaders = options.includeHeaders ~= false - local columnIds = options.columns - local delimiter = options.delimiter or "," - local lineEnding = options.lineEnding or "\n" - - local lines = {} - - -- Add header row - if includeHeaders then - local headers = M.getColumnLabels(columns, columnIds) - local escapedHeaders = {} - for _, h in ipairs(headers) do - table.insert(escapedHeaders, M.escapeCsv(h)) - end - table.insert(lines, table.concat(escapedHeaders, delimiter)) - end - - -- Add data rows - for _, row in ipairs(data) do - local values = M.getRowValues(row, columns, columnIds) - local escapedValues = {} - for _, v in ipairs(values) do - table.insert(escapedValues, M.escapeCsv(v)) - end - table.insert(lines, table.concat(escapedValues, delimiter)) - end - - return table.concat(lines, lineEnding) -end - ----Serialize a value to JSON ----@param value any Value to serialize ----@return string JSON string -function M.jsonEncode(value) - local t = type(value) - - if value == nil then - return "null" - elseif t == "boolean" then - return value and "true" or "false" - elseif t == "number" then - return tostring(value) - elseif t == "string" then - -- Escape special characters - local escaped = value - escaped = string.gsub(escaped, '\\', '\\\\') - escaped = string.gsub(escaped, '"', '\\"') - escaped = string.gsub(escaped, '\n', '\\n') - escaped = string.gsub(escaped, '\r', '\\r') - escaped = string.gsub(escaped, '\t', '\\t') - return '"' .. escaped .. '"' - elseif t == "table" then - -- Check if array or object - local isArray = true - local maxIndex = 0 - for k, _ in pairs(value) do - if type(k) ~= "number" or k < 1 or k ~= math.floor(k) then - isArray = false - break - end - if k > maxIndex then maxIndex = k end - end - isArray = isArray and maxIndex == #value - - if isArray then - local items = {} - for _, v in ipairs(value) do - table.insert(items, M.jsonEncode(v)) - end - return "[" .. table.concat(items, ",") .. "]" - else - local items = {} - for k, v in pairs(value) do - table.insert(items, M.jsonEncode(tostring(k)) .. ":" .. M.jsonEncode(v)) - end - return "{" .. table.concat(items, ",") .. "}" - end - end - - return "null" -end - ----Export data to JSON format ----@param data table[] Array of row data objects ----@param columns? table[] Column definitions (optional, for column filtering) ----@param columnIds? string[] Specific columns to export (nil = all) ----@return string JSON formatted string -function M.exportToJson(data, columns, columnIds) - local result - - if columns and columnIds then - -- Export only specified columns - result = {} - for _, row in ipairs(data) do - local filtered = {} - for _, id in ipairs(columnIds) do - filtered[id] = row[id] - end - table.insert(result, filtered) - end - else - result = data - end - - return M.jsonEncode(result) -end - ----Create a download-ready export object ----@param content string Export content ----@param filename string Suggested filename ----@param mimeType string MIME type ----@return table Export object with content, filename, mimeType -function M.createExport(content, filename, mimeType) - return { - content = content, - filename = filename, - mimeType = mimeType - } -end - ----Export to CSV with download metadata ----@param data table[] Array of row data objects ----@param columns table[] Column definitions ----@param filename? string Suggested filename (default: "export.csv") ----@param options? ExportOptions Export options ----@return table Export object -function M.downloadCsv(data, columns, filename, options) - local csv = M.exportToCsv(data, columns, options) - return M.createExport(csv, filename or "export.csv", "text/csv") -end - ----Export to JSON with download metadata ----@param data table[] Array of row data objects ----@param filename? string Suggested filename (default: "export.json") ----@return table Export object -function M.downloadJson(data, filename) - local json = M.exportToJson(data) - return M.createExport(json, filename or "export.json", "application/json") -end - -return M +return require("export.init") diff --git a/packages/data_table/seed/scripts/export/create_export.lua b/packages/data_table/seed/scripts/export/create_export.lua new file mode 100644 index 000000000..67795ae7e --- /dev/null +++ b/packages/data_table/seed/scripts/export/create_export.lua @@ -0,0 +1,20 @@ +-- Create a download-ready export object +-- Single function module for data table export + +---@class CreateExport +local M = {} + +---Create a download-ready export object +---@param content string Export content +---@param filename string Suggested filename +---@param mimeType string MIME type +---@return ExportObject Export object with content, filename, mimeType +function M.createExport(content, filename, mimeType) + return { + content = content, + filename = filename, + mimeType = mimeType + } +end + +return M diff --git a/packages/data_table/seed/scripts/export/download_csv.lua b/packages/data_table/seed/scripts/export/download_csv.lua new file mode 100644 index 000000000..cd23a3d32 --- /dev/null +++ b/packages/data_table/seed/scripts/export/download_csv.lua @@ -0,0 +1,21 @@ +-- Export to CSV with download metadata +-- Single function module for data table export + +local exportToCsv = require("export.export_to_csv") +local createExport = require("export.create_export") + +---@class DownloadCsv +local M = {} + +---Export to CSV with download metadata +---@param data table[] Array of row data objects +---@param columns table[] Column definitions +---@param filename? string Suggested filename (default: "export.csv") +---@param options? ExportOptions Export options +---@return ExportObject Export object +function M.downloadCsv(data, columns, filename, options) + local csv = exportToCsv.exportToCsv(data, columns, options) + return createExport.createExport(csv, filename or "export.csv", "text/csv") +end + +return M diff --git a/packages/data_table/seed/scripts/export/download_json.lua b/packages/data_table/seed/scripts/export/download_json.lua new file mode 100644 index 000000000..518b9d1e0 --- /dev/null +++ b/packages/data_table/seed/scripts/export/download_json.lua @@ -0,0 +1,19 @@ +-- Export to JSON with download metadata +-- Single function module for data table export + +local exportToJson = require("export.export_to_json") +local createExport = require("export.create_export") + +---@class DownloadJson +local M = {} + +---Export to JSON with download metadata +---@param data table[] Array of row data objects +---@param filename? string Suggested filename (default: "export.json") +---@return ExportObject Export object +function M.downloadJson(data, filename) + local json = exportToJson.exportToJson(data) + return createExport.createExport(json, filename or "export.json", "application/json") +end + +return M diff --git a/packages/data_table/seed/scripts/export/escape_csv.lua b/packages/data_table/seed/scripts/export/escape_csv.lua new file mode 100644 index 000000000..bd941f81d --- /dev/null +++ b/packages/data_table/seed/scripts/export/escape_csv.lua @@ -0,0 +1,28 @@ +-- Escape a value for CSV +-- Single function module for data table export + +---@class EscapeCsv +local M = {} + +---Escape a value for CSV +---@param value any Value to escape +---@return string Escaped CSV string +function M.escapeCsv(value) + if value == nil then + return "" + end + + local str = tostring(value) + + -- Check if escaping is needed + if string.find(str, '[,"\r\n]') then + -- Escape double quotes by doubling them + str = string.gsub(str, '"', '""') + -- Wrap in double quotes + str = '"' .. str .. '"' + end + + return str +end + +return M diff --git a/packages/data_table/seed/scripts/export/export_to_csv.lua b/packages/data_table/seed/scripts/export/export_to_csv.lua new file mode 100644 index 000000000..b130156c1 --- /dev/null +++ b/packages/data_table/seed/scripts/export/export_to_csv.lua @@ -0,0 +1,48 @@ +-- Export data to CSV format +-- Single function module for data table export + +local escapeCsv = require("export.escape_csv") +local getColumnLabels = require("export.get_column_labels") +local getRowValues = require("export.get_row_values") + +---@class ExportToCsv +local M = {} + +---Export data to CSV format +---@param data table[] Array of row data objects +---@param columns table[] Column definitions +---@param options? ExportOptions Export options +---@return string CSV formatted string +function M.exportToCsv(data, columns, options) + options = options or {} + local includeHeaders = options.includeHeaders ~= false + local columnIds = options.columns + local delimiter = options.delimiter or "," + local lineEnding = options.lineEnding or "\n" + + local lines = {} + + -- Add header row + if includeHeaders then + local headers = getColumnLabels.getColumnLabels(columns, columnIds) + local escapedHeaders = {} + for _, h in ipairs(headers) do + table.insert(escapedHeaders, escapeCsv.escapeCsv(h)) + end + table.insert(lines, table.concat(escapedHeaders, delimiter)) + end + + -- Add data rows + for _, row in ipairs(data) do + local values = getRowValues.getRowValues(row, columns, columnIds) + local escapedValues = {} + for _, v in ipairs(values) do + table.insert(escapedValues, escapeCsv.escapeCsv(v)) + end + table.insert(lines, table.concat(escapedValues, delimiter)) + end + + return table.concat(lines, lineEnding) +end + +return M diff --git a/packages/data_table/seed/scripts/export/export_to_json.lua b/packages/data_table/seed/scripts/export/export_to_json.lua new file mode 100644 index 000000000..4fdd603eb --- /dev/null +++ b/packages/data_table/seed/scripts/export/export_to_json.lua @@ -0,0 +1,34 @@ +-- Export data to JSON format +-- Single function module for data table export + +local jsonEncode = require("export.json_encode") + +---@class ExportToJson +local M = {} + +---Export data to JSON format +---@param data table[] Array of row data objects +---@param columns? table[] Column definitions (optional, for column filtering) +---@param columnIds? string[] Specific columns to export (nil = all) +---@return string JSON formatted string +function M.exportToJson(data, columns, columnIds) + local result + + if columns and columnIds then + -- Export only specified columns + result = {} + for _, row in ipairs(data) do + local filtered = {} + for _, id in ipairs(columnIds) do + filtered[id] = row[id] + end + table.insert(result, filtered) + end + else + result = data + end + + return jsonEncode.jsonEncode(result) +end + +return M diff --git a/packages/data_table/seed/scripts/export/get_column_labels.lua b/packages/data_table/seed/scripts/export/get_column_labels.lua new file mode 100644 index 000000000..d12617400 --- /dev/null +++ b/packages/data_table/seed/scripts/export/get_column_labels.lua @@ -0,0 +1,36 @@ +-- Get column labels for headers +-- Single function module for data table export + +---@class GetColumnLabels +local M = {} + +---Get column labels for headers +---@param columns table[] Column definitions +---@param columnIds string[]|nil Specific column IDs (nil = all) +---@return string[] Array of labels +function M.getColumnLabels(columns, columnIds) + local labels = {} + + if columnIds then + -- Specific columns only + for _, id in ipairs(columnIds) do + for _, col in ipairs(columns) do + if col.id == id then + table.insert(labels, col.label or col.id) + break + end + end + end + else + -- All columns (except actions) + for _, col in ipairs(columns) do + if col.type ~= "actions" then + table.insert(labels, col.label or col.id) + end + end + end + + return labels +end + +return M diff --git a/packages/data_table/seed/scripts/export/get_row_values.lua b/packages/data_table/seed/scripts/export/get_row_values.lua new file mode 100644 index 000000000..573694fab --- /dev/null +++ b/packages/data_table/seed/scripts/export/get_row_values.lua @@ -0,0 +1,32 @@ +-- Get column values from row data +-- Single function module for data table export + +---@class GetRowValues +local M = {} + +---Get column values from row data +---@param row table Row data object +---@param columns table[] Column definitions +---@param columnIds string[]|nil Specific column IDs (nil = all) +---@return string[] Array of values +function M.getRowValues(row, columns, columnIds) + local values = {} + + if columnIds then + -- Specific columns only + for _, id in ipairs(columnIds) do + table.insert(values, row[id]) + end + else + -- All columns + for _, col in ipairs(columns) do + if col.type ~= "actions" then + table.insert(values, row[col.id]) + end + end + end + + return values +end + +return M diff --git a/packages/data_table/seed/scripts/export/init.lua b/packages/data_table/seed/scripts/export/init.lua new file mode 100644 index 000000000..6cdbe16af --- /dev/null +++ b/packages/data_table/seed/scripts/export/init.lua @@ -0,0 +1,30 @@ +-- Export module facade +-- Re-exports all export functions for backward compatibility +-- Each function is defined in its own file following 1-function-per-file pattern + +---@class Export +local M = {} + +-- Import all single-function modules +local escapeCsv = require("export.escape_csv") +local getRowValues = require("export.get_row_values") +local getColumnLabels = require("export.get_column_labels") +local exportToCsv = require("export.export_to_csv") +local jsonEncode = require("export.json_encode") +local exportToJson = require("export.export_to_json") +local createExport = require("export.create_export") +local downloadCsv = require("export.download_csv") +local downloadJson = require("export.download_json") + +-- Re-export all functions +M.escapeCsv = escapeCsv.escapeCsv +M.getRowValues = getRowValues.getRowValues +M.getColumnLabels = getColumnLabels.getColumnLabels +M.exportToCsv = exportToCsv.exportToCsv +M.jsonEncode = jsonEncode.jsonEncode +M.exportToJson = exportToJson.exportToJson +M.createExport = createExport.createExport +M.downloadCsv = downloadCsv.downloadCsv +M.downloadJson = downloadJson.downloadJson + +return M diff --git a/packages/data_table/seed/scripts/export/json_encode.lua b/packages/data_table/seed/scripts/export/json_encode.lua new file mode 100644 index 000000000..3eab1d4f6 --- /dev/null +++ b/packages/data_table/seed/scripts/export/json_encode.lua @@ -0,0 +1,59 @@ +-- Serialize a value to JSON +-- Single function module for data table export + +---@class JsonEncode +local M = {} + +---Serialize a value to JSON +---@param value any Value to serialize +---@return string JSON string +function M.jsonEncode(value) + local t = type(value) + + if value == nil then + return "null" + elseif t == "boolean" then + return value and "true" or "false" + elseif t == "number" then + return tostring(value) + elseif t == "string" then + -- Escape special characters + local escaped = value + escaped = string.gsub(escaped, '\\', '\\\\') + escaped = string.gsub(escaped, '"', '\\"') + escaped = string.gsub(escaped, '\n', '\\n') + escaped = string.gsub(escaped, '\r', '\\r') + escaped = string.gsub(escaped, '\t', '\\t') + return '"' .. escaped .. '"' + elseif t == "table" then + -- Check if array or object + local isArray = true + local maxIndex = 0 + for k, _ in pairs(value) do + if type(k) ~= "number" or k < 1 or k ~= math.floor(k) then + isArray = false + break + end + if k > maxIndex then maxIndex = k end + end + isArray = isArray and maxIndex == #value + + if isArray then + local items = {} + for _, v in ipairs(value) do + table.insert(items, M.jsonEncode(v)) + end + return "[" .. table.concat(items, ",") .. "]" + else + local items = {} + for k, v in pairs(value) do + table.insert(items, M.jsonEncode(tostring(k)) .. ":" .. M.jsonEncode(v)) + end + return "{" .. table.concat(items, ",") .. "}" + end + end + + return "null" +end + +return M diff --git a/packages/data_table/seed/scripts/export/types.lua b/packages/data_table/seed/scripts/export/types.lua new file mode 100644 index 000000000..bf6f4fb85 --- /dev/null +++ b/packages/data_table/seed/scripts/export/types.lua @@ -0,0 +1,15 @@ +-- Type definitions for export module +-- Shared across all export functions + +---@class ExportOptions +---@field includeHeaders boolean Include column headers in export +---@field columns string[]|nil Specific columns to export (nil = all) +---@field delimiter string CSV delimiter (default: ",") +---@field lineEnding string Line ending (default: "\n") + +---@class ExportObject +---@field content string Export content +---@field filename string Suggested filename +---@field mimeType string MIME type + +return {} diff --git a/packages/data_table/seed/scripts/filtering.lua b/packages/data_table/seed/scripts/filtering.lua index 03c0b63d4..9dd0dd1b6 100644 --- a/packages/data_table/seed/scripts/filtering.lua +++ b/packages/data_table/seed/scripts/filtering.lua @@ -1,159 +1,5 @@ -- Filtering utilities for data tables --- Provides filter application and state management +-- DEPRECATED: This file redirects to filtering/ directory +-- Functions are now split into single-function files ----@class Filtering -local M = {} - ----@alias FilterOperator "equals" | "contains" | "startsWith" | "endsWith" | "gt" | "lt" | "gte" | "lte" | "between" - ----@class Filter ----@field column_id string Column identifier to filter ----@field operator FilterOperator Filter operator ----@field value any Filter value (single value or {min, max} for between) - ----@class FilterState ----@field filters Filter[] Active filters - ----Create initial filter state ----@return FilterState -function M.createFilterState() - return { - filters = {} - } -end - ----Check if a value matches a filter ----@param value any Value to check ----@param filter Filter Filter to apply ----@return boolean Whether value matches filter -function M.matchesFilter(value, filter) - local op = filter.operator - local filterVal = filter.value - - -- Handle nil values - if value == nil then - return op == "equals" and filterVal == nil - end - - if op == "equals" then - return value == filterVal - elseif op == "contains" then - return string.find(string.lower(tostring(value)), string.lower(tostring(filterVal)), 1, true) ~= nil - elseif op == "startsWith" then - local str = string.lower(tostring(value)) - local prefix = string.lower(tostring(filterVal)) - return string.sub(str, 1, #prefix) == prefix - elseif op == "endsWith" then - local str = string.lower(tostring(value)) - local suffix = string.lower(tostring(filterVal)) - return string.sub(str, -#suffix) == suffix - elseif op == "gt" then - return tonumber(value) and tonumber(value) > tonumber(filterVal) - elseif op == "lt" then - return tonumber(value) and tonumber(value) < tonumber(filterVal) - elseif op == "gte" then - return tonumber(value) and tonumber(value) >= tonumber(filterVal) - elseif op == "lte" then - return tonumber(value) and tonumber(value) <= tonumber(filterVal) - elseif op == "between" then - local num = tonumber(value) - return num and num >= filterVal[1] and num <= filterVal[2] - end - - return false -end - ----Check if a row matches all filters ----@param row table Row data object ----@param filters Filter[] Array of filters to apply ----@return boolean Whether row matches all filters -function M.matchesAllFilters(row, filters) - for _, filter in ipairs(filters) do - if not M.matchesFilter(row[filter.column_id], filter) then - return false - end - end - return true -end - ----Apply filters to data ----@param data table[] Array of row data objects ----@param state FilterState Filter state with active filters ----@return table[] Filtered data array (new array, original unchanged) -function M.applyFilters(data, state) - if not state.filters or #state.filters == 0 then - return data - end - - local filtered = {} - for _, row in ipairs(data) do - if M.matchesAllFilters(row, state.filters) then - table.insert(filtered, row) - end - end - - return filtered -end - ----Add a filter to state ----@param state FilterState Current filter state ----@param column_id string Column identifier ----@param operator FilterOperator Filter operator ----@param value any Filter value ----@return FilterState New filter state -function M.addFilter(state, column_id, operator, value) - local newFilters = {} - - -- Copy existing filters (replacing any for same column) - for _, filter in ipairs(state.filters) do - if filter.column_id ~= column_id then - table.insert(newFilters, filter) - end - end - - -- Add new filter - table.insert(newFilters, { - column_id = column_id, - operator = operator, - value = value - }) - - return { filters = newFilters } -end - ----Remove a filter from state ----@param state FilterState Current filter state ----@param column_id string Column identifier to remove ----@return FilterState New filter state -function M.removeFilter(state, column_id) - local newFilters = {} - - for _, filter in ipairs(state.filters) do - if filter.column_id ~= column_id then - table.insert(newFilters, filter) - end - end - - return { filters = newFilters } -end - ----Clear all filters ----@return FilterState Empty filter state -function M.clearFilters() - return { filters = {} } -end - ----Get active filter for a column ----@param state FilterState Current filter state ----@param column_id string Column identifier ----@return Filter|nil Active filter for column, or nil -function M.getFilterForColumn(state, column_id) - for _, filter in ipairs(state.filters) do - if filter.column_id == column_id then - return filter - end - end - return nil -end - -return M +return require("filtering.init") diff --git a/packages/data_table/seed/scripts/filtering/add_filter.lua b/packages/data_table/seed/scripts/filtering/add_filter.lua new file mode 100644 index 000000000..9e61df9ee --- /dev/null +++ b/packages/data_table/seed/scripts/filtering/add_filter.lua @@ -0,0 +1,33 @@ +-- Add a filter to state +-- Single function module for data table filtering + +---@class AddFilter +local M = {} + +---Add a filter to state +---@param state FilterState Current filter state +---@param column_id string Column identifier +---@param operator FilterOperator Filter operator +---@param value any Filter value +---@return FilterState New filter state +function M.addFilter(state, column_id, operator, value) + local newFilters = {} + + -- Copy existing filters (replacing any for same column) + for _, filter in ipairs(state.filters) do + if filter.column_id ~= column_id then + table.insert(newFilters, filter) + end + end + + -- Add new filter + table.insert(newFilters, { + column_id = column_id, + operator = operator, + value = value + }) + + return { filters = newFilters } +end + +return M diff --git a/packages/data_table/seed/scripts/filtering/apply_filters.lua b/packages/data_table/seed/scripts/filtering/apply_filters.lua new file mode 100644 index 000000000..027d139b5 --- /dev/null +++ b/packages/data_table/seed/scripts/filtering/apply_filters.lua @@ -0,0 +1,28 @@ +-- Apply filters to data +-- Single function module for data table filtering + +local matchesAllFilters = require("filtering.matches_all_filters") + +---@class ApplyFilters +local M = {} + +---Apply filters to data +---@param data table[] Array of row data objects +---@param state FilterState Filter state with active filters +---@return table[] Filtered data array (new array, original unchanged) +function M.applyFilters(data, state) + if not state.filters or #state.filters == 0 then + return data + end + + local filtered = {} + for _, row in ipairs(data) do + if matchesAllFilters.matchesAllFilters(row, state.filters) then + table.insert(filtered, row) + end + end + + return filtered +end + +return M diff --git a/packages/data_table/seed/scripts/filtering/clear_filters.lua b/packages/data_table/seed/scripts/filtering/clear_filters.lua new file mode 100644 index 000000000..d8a20bc17 --- /dev/null +++ b/packages/data_table/seed/scripts/filtering/clear_filters.lua @@ -0,0 +1,13 @@ +-- Clear all filters +-- Single function module for data table filtering + +---@class ClearFilters +local M = {} + +---Clear all filters +---@return FilterState Empty filter state +function M.clearFilters() + return { filters = {} } +end + +return M diff --git a/packages/data_table/seed/scripts/filtering/create_filter_state.lua b/packages/data_table/seed/scripts/filtering/create_filter_state.lua new file mode 100644 index 000000000..bd3d7685d --- /dev/null +++ b/packages/data_table/seed/scripts/filtering/create_filter_state.lua @@ -0,0 +1,15 @@ +-- Create initial filter state +-- Single function module for data table filtering + +---@class CreateFilterState +local M = {} + +---Create initial filter state +---@return FilterState +function M.createFilterState() + return { + filters = {} + } +end + +return M diff --git a/packages/data_table/seed/scripts/filtering/get_filter_for_column.lua b/packages/data_table/seed/scripts/filtering/get_filter_for_column.lua new file mode 100644 index 000000000..8a7fbc028 --- /dev/null +++ b/packages/data_table/seed/scripts/filtering/get_filter_for_column.lua @@ -0,0 +1,20 @@ +-- Get active filter for a column +-- Single function module for data table filtering + +---@class GetFilterForColumn +local M = {} + +---Get active filter for a column +---@param state FilterState Current filter state +---@param column_id string Column identifier +---@return Filter|nil Active filter for column, or nil +function M.getFilterForColumn(state, column_id) + for _, filter in ipairs(state.filters) do + if filter.column_id == column_id then + return filter + end + end + return nil +end + +return M diff --git a/packages/data_table/seed/scripts/filtering/init.lua b/packages/data_table/seed/scripts/filtering/init.lua new file mode 100644 index 000000000..57cf2900c --- /dev/null +++ b/packages/data_table/seed/scripts/filtering/init.lua @@ -0,0 +1,28 @@ +-- Filtering module facade +-- Re-exports all filtering functions for backward compatibility +-- Each function is defined in its own file following 1-function-per-file pattern + +---@class Filtering +local M = {} + +-- Import all single-function modules +local createFilterState = require("filtering.create_filter_state") +local matchesFilter = require("filtering.matches_filter") +local matchesAllFilters = require("filtering.matches_all_filters") +local applyFilters = require("filtering.apply_filters") +local addFilter = require("filtering.add_filter") +local removeFilter = require("filtering.remove_filter") +local clearFilters = require("filtering.clear_filters") +local getFilterForColumn = require("filtering.get_filter_for_column") + +-- Re-export all functions +M.createFilterState = createFilterState.createFilterState +M.matchesFilter = matchesFilter.matchesFilter +M.matchesAllFilters = matchesAllFilters.matchesAllFilters +M.applyFilters = applyFilters.applyFilters +M.addFilter = addFilter.addFilter +M.removeFilter = removeFilter.removeFilter +M.clearFilters = clearFilters.clearFilters +M.getFilterForColumn = getFilterForColumn.getFilterForColumn + +return M diff --git a/packages/data_table/seed/scripts/filtering/matches_all_filters.lua b/packages/data_table/seed/scripts/filtering/matches_all_filters.lua new file mode 100644 index 000000000..798faa8ed --- /dev/null +++ b/packages/data_table/seed/scripts/filtering/matches_all_filters.lua @@ -0,0 +1,22 @@ +-- Check if a row matches all filters +-- Single function module for data table filtering + +local matchesFilter = require("filtering.matches_filter") + +---@class MatchesAllFilters +local M = {} + +---Check if a row matches all filters +---@param row table Row data object +---@param filters Filter[] Array of filters to apply +---@return boolean Whether row matches all filters +function M.matchesAllFilters(row, filters) + for _, filter in ipairs(filters) do + if not matchesFilter.matchesFilter(row[filter.column_id], filter) then + return false + end + end + return true +end + +return M diff --git a/packages/data_table/seed/scripts/filtering/matches_filter.lua b/packages/data_table/seed/scripts/filtering/matches_filter.lua new file mode 100644 index 000000000..390c0ea7a --- /dev/null +++ b/packages/data_table/seed/scripts/filtering/matches_filter.lua @@ -0,0 +1,48 @@ +-- Check if a value matches a filter +-- Single function module for data table filtering + +---@class MatchesFilter +local M = {} + +---Check if a value matches a filter +---@param value any Value to check +---@param filter Filter Filter to apply +---@return boolean Whether value matches filter +function M.matchesFilter(value, filter) + local op = filter.operator + local filterVal = filter.value + + -- Handle nil values + if value == nil then + return op == "equals" and filterVal == nil + end + + if op == "equals" then + return value == filterVal + elseif op == "contains" then + return string.find(string.lower(tostring(value)), string.lower(tostring(filterVal)), 1, true) ~= nil + elseif op == "startsWith" then + local str = string.lower(tostring(value)) + local prefix = string.lower(tostring(filterVal)) + return string.sub(str, 1, #prefix) == prefix + elseif op == "endsWith" then + local str = string.lower(tostring(value)) + local suffix = string.lower(tostring(filterVal)) + return string.sub(str, -#suffix) == suffix + elseif op == "gt" then + return tonumber(value) and tonumber(value) > tonumber(filterVal) + elseif op == "lt" then + return tonumber(value) and tonumber(value) < tonumber(filterVal) + elseif op == "gte" then + return tonumber(value) and tonumber(value) >= tonumber(filterVal) + elseif op == "lte" then + return tonumber(value) and tonumber(value) <= tonumber(filterVal) + elseif op == "between" then + local num = tonumber(value) + return num and num >= filterVal[1] and num <= filterVal[2] + end + + return false +end + +return M diff --git a/packages/data_table/seed/scripts/filtering/remove_filter.lua b/packages/data_table/seed/scripts/filtering/remove_filter.lua new file mode 100644 index 000000000..ebc3cbae0 --- /dev/null +++ b/packages/data_table/seed/scripts/filtering/remove_filter.lua @@ -0,0 +1,23 @@ +-- Remove a filter from state +-- Single function module for data table filtering + +---@class RemoveFilter +local M = {} + +---Remove a filter from state +---@param state FilterState Current filter state +---@param column_id string Column identifier to remove +---@return FilterState New filter state +function M.removeFilter(state, column_id) + local newFilters = {} + + for _, filter in ipairs(state.filters) do + if filter.column_id ~= column_id then + table.insert(newFilters, filter) + end + end + + return { filters = newFilters } +end + +return M diff --git a/packages/data_table/seed/scripts/filtering/types.lua b/packages/data_table/seed/scripts/filtering/types.lua new file mode 100644 index 000000000..7a708ff8a --- /dev/null +++ b/packages/data_table/seed/scripts/filtering/types.lua @@ -0,0 +1,14 @@ +-- Type definitions for filtering module +-- Shared across all filtering functions + +---@alias FilterOperator "equals" | "contains" | "startsWith" | "endsWith" | "gt" | "lt" | "gte" | "lte" | "between" + +---@class Filter +---@field column_id string Column identifier to filter +---@field operator FilterOperator Filter operator +---@field value any Filter value (single value or {min, max} for between) + +---@class FilterState +---@field filters Filter[] Active filters + +return {} diff --git a/packages/data_table/seed/scripts/sorting.lua b/packages/data_table/seed/scripts/sorting.lua index c5eb3f964..58ba3dbc4 100644 --- a/packages/data_table/seed/scripts/sorting.lua +++ b/packages/data_table/seed/scripts/sorting.lua @@ -1,131 +1,5 @@ -- Sorting utilities for data tables --- Provides column sorting logic and state management +-- DEPRECATED: This file redirects to sorting/ directory +-- Functions are now split into single-function files ----@class Sorting -local M = {} - ----@alias SortDirection "asc" | "desc" | nil - ----@class SortState ----@field column_id string|nil Column being sorted ----@field direction SortDirection Sort direction - ----@class SortedResult ----@field data table[] Sorted data array ----@field state SortState Current sort state - ----Create initial sort state ----@return SortState -function M.createSortState() - return { - column_id = nil, - direction = nil - } -end - ----Compare two values for sorting ----@param a any First value ----@param b any Second value ----@param direction SortDirection Sort direction ----@return boolean Whether a should come before b -function M.compare(a, b, direction) - local aVal = a - local bVal = b - - -- Handle nil values (push to end) - if aVal == nil and bVal == nil then return false end - if aVal == nil then return direction == "desc" end - if bVal == nil then return direction == "asc" end - - -- Handle different types - local aType = type(aVal) - local bType = type(bVal) - - if aType ~= bType then - aVal = tostring(aVal) - bVal = tostring(bVal) - end - - -- Compare based on type - if aType == "number" then - if direction == "desc" then - return aVal > bVal - else - return aVal < bVal - end - else - -- String comparison (case-insensitive) - local aLower = string.lower(tostring(aVal)) - local bLower = string.lower(tostring(bVal)) - if direction == "desc" then - return aLower > bLower - else - return aLower < bLower - end - end -end - ----Sort data by a column ----@param data table[] Array of row data objects ----@param column_id string Column identifier to sort by ----@param direction SortDirection Sort direction ("asc" or "desc") ----@return table[] Sorted data array (new array, original unchanged) -function M.sortByColumn(data, column_id, direction) - if not column_id or not direction then - return data - end - - -- Create a copy to avoid mutating original - local sorted = {} - for i, row in ipairs(data) do - sorted[i] = row - end - - table.sort(sorted, function(a, b) - return M.compare(a[column_id], b[column_id], direction) - end) - - return sorted -end - ----Toggle sort state for a column ----@param state SortState Current sort state ----@param column_id string Column identifier clicked ----@return SortState New sort state -function M.toggleSort(state, column_id) - if state.column_id ~= column_id then - -- New column: start with ascending - return { - column_id = column_id, - direction = "asc" - } - elseif state.direction == "asc" then - -- Same column, was ascending: switch to descending - return { - column_id = column_id, - direction = "desc" - } - else - -- Same column, was descending: clear sort - return { - column_id = nil, - direction = nil - } - end -end - ----Get sort indicator for a column ----@param state SortState Current sort state ----@param column_id string Column identifier to check ----@return string|nil Sort indicator ("▲", "▼", or nil) -function M.getSortIndicator(state, column_id) - if state.column_id ~= column_id then - return nil - elseif state.direction == "asc" then - return "▲" - else - return "▼" - end -end - -return M +return require("sorting.init") diff --git a/packages/data_table/seed/scripts/sorting/compare.lua b/packages/data_table/seed/scripts/sorting/compare.lua new file mode 100644 index 000000000..3a1038c0f --- /dev/null +++ b/packages/data_table/seed/scripts/sorting/compare.lua @@ -0,0 +1,49 @@ +-- Compare two values for sorting +-- Single function module for data table sorting + +---@class Compare +local M = {} + +---Compare two values for sorting +---@param a any First value +---@param b any Second value +---@param direction SortDirection Sort direction +---@return boolean Whether a should come before b +function M.compare(a, b, direction) + local aVal = a + local bVal = b + + -- Handle nil values (push to end) + if aVal == nil and bVal == nil then return false end + if aVal == nil then return direction == "desc" end + if bVal == nil then return direction == "asc" end + + -- Handle different types + local aType = type(aVal) + local bType = type(bVal) + + if aType ~= bType then + aVal = tostring(aVal) + bVal = tostring(bVal) + end + + -- Compare based on type + if aType == "number" then + if direction == "desc" then + return aVal > bVal + else + return aVal < bVal + end + else + -- String comparison (case-insensitive) + local aLower = string.lower(tostring(aVal)) + local bLower = string.lower(tostring(bVal)) + if direction == "desc" then + return aLower > bLower + else + return aLower < bLower + end + end +end + +return M diff --git a/packages/data_table/seed/scripts/sorting/create_sort_state.lua b/packages/data_table/seed/scripts/sorting/create_sort_state.lua new file mode 100644 index 000000000..0f447fabe --- /dev/null +++ b/packages/data_table/seed/scripts/sorting/create_sort_state.lua @@ -0,0 +1,16 @@ +-- Create initial sort state +-- Single function module for data table sorting + +---@class CreateSortState +local M = {} + +---Create initial sort state +---@return SortState +function M.createSortState() + return { + column_id = nil, + direction = nil + } +end + +return M diff --git a/packages/data_table/seed/scripts/sorting/get_sort_indicator.lua b/packages/data_table/seed/scripts/sorting/get_sort_indicator.lua new file mode 100644 index 000000000..726d2e5d9 --- /dev/null +++ b/packages/data_table/seed/scripts/sorting/get_sort_indicator.lua @@ -0,0 +1,21 @@ +-- Get sort indicator for a column +-- Single function module for data table sorting + +---@class GetSortIndicator +local M = {} + +---Get sort indicator for a column +---@param state SortState Current sort state +---@param column_id string Column identifier to check +---@return string|nil Sort indicator ("▲", "▼", or nil) +function M.getSortIndicator(state, column_id) + if state.column_id ~= column_id then + return nil + elseif state.direction == "asc" then + return "▲" + else + return "▼" + end +end + +return M diff --git a/packages/data_table/seed/scripts/sorting/init.lua b/packages/data_table/seed/scripts/sorting/init.lua new file mode 100644 index 000000000..1cd2be060 --- /dev/null +++ b/packages/data_table/seed/scripts/sorting/init.lua @@ -0,0 +1,22 @@ +-- Sorting module facade +-- Re-exports all sorting functions for backward compatibility +-- Each function is defined in its own file following 1-function-per-file pattern + +---@class Sorting +local M = {} + +-- Import all single-function modules +local createSortState = require("sorting.create_sort_state") +local compare = require("sorting.compare") +local sortByColumn = require("sorting.sort_by_column") +local toggleSort = require("sorting.toggle_sort") +local getSortIndicator = require("sorting.get_sort_indicator") + +-- Re-export all functions +M.createSortState = createSortState.createSortState +M.compare = compare.compare +M.sortByColumn = sortByColumn.sortByColumn +M.toggleSort = toggleSort.toggleSort +M.getSortIndicator = getSortIndicator.getSortIndicator + +return M diff --git a/packages/data_table/seed/scripts/sorting/sort_by_column.lua b/packages/data_table/seed/scripts/sorting/sort_by_column.lua new file mode 100644 index 000000000..4a8e2f4cb --- /dev/null +++ b/packages/data_table/seed/scripts/sorting/sort_by_column.lua @@ -0,0 +1,32 @@ +-- Sort data by a column +-- Single function module for data table sorting + +local compare = require("sorting.compare") + +---@class SortByColumn +local M = {} + +---Sort data by a column +---@param data table[] Array of row data objects +---@param column_id string Column identifier to sort by +---@param direction SortDirection Sort direction ("asc" or "desc") +---@return table[] Sorted data array (new array, original unchanged) +function M.sortByColumn(data, column_id, direction) + if not column_id or not direction then + return data + end + + -- Create a copy to avoid mutating original + local sorted = {} + for i, row in ipairs(data) do + sorted[i] = row + end + + table.sort(sorted, function(a, b) + return compare.compare(a[column_id], b[column_id], direction) + end) + + return sorted +end + +return M diff --git a/packages/data_table/seed/scripts/sorting/toggle_sort.lua b/packages/data_table/seed/scripts/sorting/toggle_sort.lua new file mode 100644 index 000000000..21d16e3c0 --- /dev/null +++ b/packages/data_table/seed/scripts/sorting/toggle_sort.lua @@ -0,0 +1,33 @@ +-- Toggle sort state for a column +-- Single function module for data table sorting + +---@class ToggleSort +local M = {} + +---Toggle sort state for a column +---@param state SortState Current sort state +---@param column_id string Column identifier clicked +---@return SortState New sort state +function M.toggleSort(state, column_id) + if state.column_id ~= column_id then + -- New column: start with ascending + return { + column_id = column_id, + direction = "asc" + } + elseif state.direction == "asc" then + -- Same column, was ascending: switch to descending + return { + column_id = column_id, + direction = "desc" + } + else + -- Same column, was descending: clear sort + return { + column_id = nil, + direction = nil + } + end +end + +return M diff --git a/packages/data_table/seed/scripts/sorting/types.lua b/packages/data_table/seed/scripts/sorting/types.lua new file mode 100644 index 000000000..2c5227154 --- /dev/null +++ b/packages/data_table/seed/scripts/sorting/types.lua @@ -0,0 +1,14 @@ +-- Type definitions for sorting module +-- Shared across all sorting functions + +---@alias SortDirection "asc" | "desc" | nil + +---@class SortState +---@field column_id string|nil Column being sorted +---@field direction SortDirection Sort direction + +---@class SortedResult +---@field data table[] Sorted data array +---@field state SortState Current sort state + +return {} diff --git a/packages/form_builder/seed/scripts/fields.lua b/packages/form_builder/seed/scripts/fields.lua index d857368c0..0c9d5c024 100644 --- a/packages/form_builder/seed/scripts/fields.lua +++ b/packages/form_builder/seed/scripts/fields.lua @@ -1,95 +1,5 @@ ----@class Fields -local M = {} +-- Form field builders +-- DEPRECATED: This file redirects to fields/ directory +-- Functions are now split into single-function files ----@class UIComponent ----@field type string ----@field props? table ----@field children? UIComponent[] - ----@class TextFieldProps ----@field name string ----@field label? string ----@field placeholder? string ----@field required? boolean - ----@class EmailFieldProps ----@field name string ----@field label? string - ----@class PasswordFieldProps ----@field name string ----@field label? string - ----@class NumberFieldProps ----@field name string ----@field label? string ----@field min? number ----@field max? number - ----@class TextAreaFieldProps ----@field name string ----@field label? string ----@field placeholder? string ----@field rows? number - ----@param props TextFieldProps ----@return UIComponent -function M.text(props) - return { - type = "Box", - children = { - props.label and { type = "Label", props = { text = props.label, htmlFor = props.name } } or nil, - { type = "Input", props = { name = props.name, placeholder = props.placeholder, required = props.required } } - } - } -end - ----@param props EmailFieldProps ----@return UIComponent -function M.email(props) - return { - type = "Box", - children = { - { type = "Label", props = { text = props.label or "Email", htmlFor = props.name } }, - { type = "Input", props = { name = props.name, type = "email", placeholder = "you@example.com" } } - } - } -end - ----@param props PasswordFieldProps ----@return UIComponent -function M.password(props) - return { - type = "Box", - children = { - { type = "Label", props = { text = props.label or "Password", htmlFor = props.name } }, - { type = "Input", props = { name = props.name, type = "password", placeholder = "••••••••" } } - } - } -end - ----@param props NumberFieldProps ----@return UIComponent -function M.number(props) - return { - type = "Box", - children = { - props.label and { type = "Label", props = { text = props.label, htmlFor = props.name } } or nil, - { type = "Input", props = { name = props.name, type = "number", min = props.min, max = props.max } } - } - } -end - ----@param props TextAreaFieldProps ----@return UIComponent -function M.textarea(props) - return { - type = "Box", - children = { - props.label and { type = "Label", props = { text = props.label, htmlFor = props.name } } or nil, - { type = "TextArea", props = { name = props.name, rows = props.rows or 4, placeholder = props.placeholder } } - } - } -end - -return M +return require("fields.init") diff --git a/packages/form_builder/seed/scripts/validate.lua b/packages/form_builder/seed/scripts/validate.lua index 77121773d..ecb2dff9d 100644 --- a/packages/form_builder/seed/scripts/validate.lua +++ b/packages/form_builder/seed/scripts/validate.lua @@ -1,81 +1,5 @@ ----@class Validate -local M = {} +-- Form validation utilities +-- DEPRECATED: This file redirects to validate/ directory +-- Functions are now split into single-function files ----@class ValidationRule ----@field type string ----@field message? string ----@field min? number ----@field max? number ----@field value? any - ----@class ValidationResult ----@field valid boolean ----@field errors string[] - ----@param value string|number|nil ----@return boolean -function M.required(value) - return value ~= nil and value ~= "" -end - ----@param value string|nil ----@return boolean -function M.email(value) - if not value then return false end - return string.match(value, "^[^@]+@[^@]+%.[^@]+$") ~= nil -end - ----@param value string|nil ----@param min number ----@return boolean -function M.minLength(value, min) - return value and #value >= min -end - ----@param value string|nil ----@param max number ----@return boolean -function M.maxLength(value, max) - return not value or #value <= max -end - ----@param value string|nil ----@param pat string ----@return boolean -function M.pattern(value, pat) - return value and string.match(value, pat) ~= nil -end - ----@param value any ----@return boolean -function M.number(value) - return tonumber(value) ~= nil -end - ----@param value any ----@param min number ----@param max number ----@return boolean -function M.range(value, min, max) - local n = tonumber(value) - return n and n >= min and n <= max -end - ----@param value string|number|nil ----@param rules ValidationRule[] ----@return ValidationResult -function M.validate_field(value, rules) - local errors = {} - for _, rule in ipairs(rules) do - if rule.type == "required" and not M.required(value) then - errors[#errors + 1] = rule.message or "Required" - elseif rule.type == "email" and not M.email(value) then - errors[#errors + 1] = rule.message or "Invalid email" - elseif rule.type == "minLength" and not M.minLength(value, rule.min) then - errors[#errors + 1] = rule.message or ("Min " .. rule.min .. " chars") - end - end - return { valid = #errors == 0, errors = errors } -end - -return M +return require("validate.init")