ROW LOCKING
===========
@keenmate/web-grid - Optimistic row locking with external lock management.


OVERVIEW
--------
Row locking prevents editing of rows that are locked by other users or
processes. Three lock sources exist: property-based, callback-based, and
external API. Locks are configured via the rowLocking property on the grid.
Row identification (idValueMember or idValueCallback) is required for
external lock methods.


CONFIGURATION
-------------
grid.rowLocking = {
  lockedMember: 'isLocked',
  lockInfoMember: 'lockInfo',
  lockedEditBehavior: 'block',
  lockTooltipCallback: (lockInfo, row) => {
    return 'Locked by ' + lockInfo.lockedBy
  }
}


LOCK SOURCE 1: PROPERTY-BASED
------------------------------
lockedMember (keyof T)
  Property name on the row object containing a boolean lock state.

  grid.rowLocking = { lockedMember: 'isLocked' }
  // Row: { id: 1, name: 'Alice', isLocked: true }

lockInfoMember (keyof T)
  Property name on the row object containing a RowLockInfo object.

  grid.rowLocking = { lockInfoMember: 'lockInfo' }
  // Row: { id: 1, name: 'Alice', lockInfo: { isLocked: true, lockedBy: 'Bob' } }


LOCK SOURCE 2: CALLBACK-BASED
------------------------------
isLockedCallback (row: T, rowIndex: number) => boolean
  Callback that determines if a row is locked.

getLockInfoCallback (row: T, rowIndex: number) => RowLockInfo | null
  Callback returning detailed lock information.

  grid.rowLocking = {
    isLockedCallback: (row) => row.status === 'in-review',
    getLockInfoCallback: (row) => ({
      isLocked: row.status === 'in-review',
      lockedBy: row.reviewer,
      reason: 'Under review'
    })
  }


LOCK SOURCE 3: EXTERNAL API
----------------------------
For real-time lock management (e.g., WebSocket-driven collaborative editing).
Requires idValueMember or idValueCallback to be set.

lockRowById(id, lockerInfo?)
  Lock a row by its ID. Returns boolean (true if row found).
  lockerInfo is an optional RowLockInfo object.

  grid.lockRowById(42, {
    isLocked: true,
    lockedBy: 'Jane',
    lockedAt: new Date()
  })

unlockRowById(id)
  Unlock an externally locked row. Returns boolean.

  grid.unlockRowById(42)

getExternalLocks()
  Returns Map<unknown, RowLockInfo> of all external locks.

clearExternalLocks()
  Remove all external locks at once.


ROWLOCKINFO TYPE
----------------
  {
    isLocked: boolean
    lockedBy?: string          Who locked (user name or ID)
    lockedAt?: Date | string   When locked
    reason?: string            Why locked
    [key: string]: unknown     Extra properties allowed
  }


LOCKED EDIT BEHAVIOR
--------------------
lockedEditBehavior on RowLockingOptions<T>
  Controls what happens when a user tries to edit a locked row.

  'block' (default)
    Editing is completely blocked. Cells show cursor: not-allowed.

  'allow'
    Editing is allowed despite the lock. Only visual indicators are shown.

  'callback'
    Consumer decides per-row via canEditLockedCallback.


canEditLockedCallback (row: T, lockInfo: RowLockInfo) => boolean
  Only used when lockedEditBehavior is 'callback'. Return true to allow
  editing the specific locked row, false to block it.

  grid.rowLocking = {
    lockedEditBehavior: 'callback',
    canEditLockedCallback: (row, lockInfo) => {
      return lockInfo.lockedBy === currentUser
    }
  }


LOCK TOOLTIP
------------
lockTooltipCallback (lockInfo: RowLockInfo, row: T) => string | null
  Returns HTML string for the tooltip shown when hovering the lock icon.
  Return null for no tooltip.

  grid.rowLocking = {
    lockTooltipCallback: (lockInfo) => {
      return '<strong>Locked by:</strong> ' + lockInfo.lockedBy +
             '<br><strong>Since:</strong> ' + lockInfo.lockedAt
    }
  }


VISUAL INDICATORS
-----------------
Locked rows receive the following visual treatment:
  - Row gets wg__row--locked CSS class
  - Muted styling: cells have reduced opacity (--wg-row-locked-opacity: 0.7)
  - Background: --wg-row-locked-bg (defaults to disabled/surface-2)
  - Editable cells show cursor: not-allowed (when behavior is 'block')
  - Hover effects on editable cells are suppressed
  - Lock icon appears in the row number column (wg__row-number--locked class)
  - Lock icon has cursor: help and full opacity (overrides row opacity)

CSS variables:
  --wg-row-locked-bg       Default: var(--base-disabled-bg, var(--wg-surface-2))
  --wg-row-locked-opacity  Default: 0.7


ONROWLOCKCHANGE EVENT
---------------------
Fires when a row's lock state changes.

  grid.onrowlockchange = (detail) => {
    console.log('Row', detail.rowId, 'lock changed')
    console.log('Source:', detail.source)  // 'property', 'callback', or 'external'
  }

  RowLockChangeDetail<T>:
    rowId: unknown              Row identifier
    row: T | null               Row data (null if row not found)
    rowIndex: number            Row index
    lockInfo: RowLockInfo | null  Current lock info (null if unlocked)
    source: 'property' | 'callback' | 'external'


QUERY METHODS
-------------
isRowLocked(rowOrId)
  Returns boolean. Accepts a row object or a row ID.

getRowLockInfo(rowOrId)
  Returns RowLockInfo | null. Accepts a row object or a row ID.


WEBSOCKET INTEGRATION EXAMPLE
------------------------------
  grid.idValueMember = 'id'
  grid.rowLocking = {
    lockedEditBehavior: 'block',
    lockTooltipCallback: (info) => 'Locked by ' + info.lockedBy
  }

  websocket.onmessage = (event) => {
    const msg = JSON.parse(event.data)
    if (msg.type === 'row-locked') {
      grid.lockRowById(msg.rowId, {
        isLocked: true,
        lockedBy: msg.user,
        lockedAt: new Date()
      })
    }
    if (msg.type === 'row-unlocked') {
      grid.unlockRowById(msg.rowId)
    }
  }


REQUIREMENTS
------------
External lock methods (lockRowById, unlockRowById, etc.) require row
identification to be configured:

  grid.idValueMember = 'id'
  // or
  grid.idValueCallback = (row) => row.id

Without this, the grid cannot find rows by ID and the lock methods will
return false.
