Skip to content

BaseGridPlugin

Abstract base class for all grid plugins.

new BaseGridPlugin(config: Partial<TConfig>)
PropertyTypeDescription
dependencies?PluginDependency[]Plugin dependencies - declare other plugins this one requires.
manifest?PluginManifest<any>Plugin manifest - declares owned properties, config rules, and hook priorities.
namestringUnique plugin identifier (derived from class name by default)
aliases?readonly string[]Alternative names for backward compatibility. getPluginByName() matches against both name and aliases.
versionstringPlugin version - defaults to grid version for built-in plugins. Third-party plugins can override with their own semver.
styles?stringCSS styles to inject via document.adoptedStyleSheets
cellRenderers?Record<string, CellRenderer>Custom cell renderers keyed by type name
headerRenderers?Record<string, HeaderRenderer>Custom header renderers keyed by type name
cellEditors?Record<string, CellEditor>Custom cell editors keyed by type name
gridGridElementThe grid instance this plugin is attached to
configTConfigPlugin configuration - merged with defaults in attach()
userConfigPartial<TConfig>User-provided configuration from constructor

Plugin dependencies - declare other plugins this one requires.

Dependencies are validated when the plugin is attached. Required dependencies throw an error if missing. Optional dependencies log an info message if missing.

static readonly dependencies: PluginDependency[] = [
{ name: 'editing', required: true, reason: 'Tracks cell edits for undo/redo' },
{ name: 'selection', required: false, reason: 'Enables selection-based undo' },
];

Plugin manifest - declares owned properties, config rules, and hook priorities.

This is read by the configuration validator to:

  • Validate that required plugins are loaded when their properties are used
  • Execute configRules to detect invalid/conflicting settings
  • Order hook execution based on priority
static override readonly manifest: PluginManifest<MyConfig> = {
ownedProperties: [
{ property: 'myProp', level: 'column', description: 'the "myProp" column property' },
],
configRules: [
{ id: 'myPlugin/conflict', severity: 'warn', message: '...', check: (c) => c.a && c.b },
],
};

Default configuration - subclasses should override this getter. Note: This must be a getter (not property initializer) for proper inheritance since property initializers run after parent constructor.

readonly defaultConfig: Partial<TConfig>

Get the current rows from the grid.

readonly rows: any[]

Get the original unfiltered/unprocessed rows from the grid. Use this when you need all source data regardless of active filters.

readonly sourceRows: any[]

Get the current columns from the grid.

readonly columns: ColumnConfig<any>[]

Get only visible columns from the grid (excludes hidden). Use this for rendering that needs to match the grid template.

readonly visibleColumns: ColumnConfig<any>[]

Get the grid as an HTMLElement for direct DOM operations. Use sparingly - prefer the typed GridElementRef API when possible.

readonly gridElement: HTMLElement
const width = this.gridElement.clientWidth;
this.gridElement.classList.add('my-plugin-active');

Get the disconnect signal for event listener cleanup. This signal is aborted when the grid disconnects from the DOM. Use this when adding event listeners that should be cleaned up automatically.

Best for:

  • Document/window-level listeners added in attach()
  • Listeners on the grid element itself
  • Any listener that should persist across renders

Not needed for:

  • Listeners on elements created in afterRender() (removed with element)
readonly disconnectSignal: AbortSignal
element.addEventListener('click', handler, { signal: this.disconnectSignal });
document.addEventListener('keydown', handler, { signal: this.disconnectSignal });

Get the grid-level icons configuration. Returns merged icons (user config + defaults).

readonly gridIcons: Required<GridIcons>

Check if animations are enabled at the grid level. Respects gridConfig.animation.mode and the CSS variable set by the grid.

Plugins should use this to skip animations when:

  • Animation mode is ‘off’ or false
  • User prefers reduced motion and mode is ‘reduced-motion’ (default)
readonly isAnimationEnabled: boolean
private get animationStyle(): 'slide' | 'fade' | false {
if (!this.isAnimationEnabled) return false;
return this.config.animation ?? 'slide';
}

Get the animation duration in milliseconds from CSS variable. Falls back to 200ms if not set.

Plugins can use this for their animation timing to stay consistent with the grid-level animation.duration setting.

readonly animationDuration: number
element.animate(keyframes, { duration: this.animationDuration });

Merge user-supplied configuration from other plugin instances into this plugin’s userConfig. Used by PluginManager.attachAll()’s alias-collapse pre-pass: when a consumer instantiates the same plugin under multiple names (e.g. RowReorderPlugin and RowDragDropPlugin, which are aliases after V2.x), only one instance is attached, and the other instances’ configs are folded in via this method.

Merge rules (shallow):

  • Same key, same value → silent.
  • Same key, different values (incl. function vs function) → throws a diagnostic naming the conflicting key. Conflicting callbacks (canDrag, canDrop, etc.) cannot be reconciled automatically.
  • Disjoint keys → merged cleanly.

Subclasses may override to implement custom deep-merge logic, but should call super.mergeConfigsFrom(others) to inherit the conflict-detection.

Plugin infrastructure (used by PluginManager.attachAll).

mergeConfigsFrom(others: readonly BaseGridPlugin<TConfig>[]): void
NameTypeDescription
othersreadonly BaseGridPlugin<TConfig>[]

Called when the plugin is attached to a grid. Override to set up event listeners, initialize state, etc.

attach(grid: GridElement): void
NameTypeDescription
gridGridElement
attach(grid: GridElement): void {
super.attach(grid);
// Set up document-level listeners with auto-cleanup
document.addEventListener('keydown', this.handleEscape, {
signal: this.disconnectSignal
});
}

Called when the plugin is detached from a grid. Override to clean up event listeners, timers, etc.

detach(): void
detach(): void {
// Clean up any state not handled by disconnectSignal
this.selectedRows.clear();
this.cache = null;
}

Called when another plugin is attached to the same grid. Use for inter-plugin coordination, e.g., to integrate with new plugins.

onPluginAttached(name: string, plugin: BaseGridPlugin): void
NameTypeDescription
namestringThe name of the plugin that was attached
pluginBaseGridPluginThe plugin instance (for direct access if needed)
onPluginAttached(name: string, plugin: BaseGridPlugin): void {
if (name === 'selection') {
// Integrate with selection plugin
this.selectionPlugin = plugin as SelectionPlugin;
}
}

Called when another plugin is detached from the same grid. Use to clean up inter-plugin references.

onPluginDetached(name: string): void
NameTypeDescription
namestringThe name of the plugin that was detached
onPluginDetached(name: string): void {
if (name === 'selection') {
this.selectionPlugin = undefined;
}
}

Get another plugin instance from the same grid. Use for inter-plugin communication.

Prefer grid.getPluginByName() when you don’t need the class import.

getPlugin(PluginClass: (args: any[]) => T): T | undefined
NameTypeDescription
PluginClass(args: any[]) => T
// Preferred: by name
const selection = this.grid?.getPluginByName('selection');
// Alternative: by class
const selection = this.getPlugin(SelectionPlugin);
if (selection) {
const selectedRows = selection.getSelectedRows();
}

Emit a custom event from the grid.

emit(eventName: string, detail: T): void
NameTypeDescription
eventNamestring
detailT

Emit a cancelable custom event from the grid.

emitCancelable(eventName: string, detail: T): boolean
NameTypeDescription
eventNamestring
detailT

boolean - true if the event was cancelled (preventDefault called), false otherwise


Subscribe to an event from another plugin. The subscription is automatically cleaned up when this plugin is detached.

on(eventType: string, callback: (detail: T) => void): void
NameTypeDescription
eventTypestringThe event type to listen for (e.g., ‘filter-change’)
callback(detail: T) => voidThe callback to invoke when the event is emitted
// In attach() or other initialization
this.on('filter-change', (detail) => {
console.log('Filter changed:', detail);
});

Unsubscribe from a plugin event.

off(eventType: string): void
NameTypeDescription
eventTypestringThe event type to stop listening for
this.off('filter-change');

Emit an event to other plugins via the Event Bus. This is for inter-plugin communication only; it does NOT dispatch DOM events. Use emit() to dispatch DOM events that external consumers can listen to.

emitPluginEvent(eventType: string, detail: T): void
NameTypeDescription
eventTypestringThe event type to emit (should be declared in manifest.events)
detailTThe event payload
// Emit to other plugins (not DOM)
this.emitPluginEvent('filter-change', { field: 'name', value: 'Alice' });
// For DOM events that consumers can addEventListener to:
this.emit('filter-change', { field: 'name', value: 'Alice' });

Emit an event to both the plugin Event Bus (for inter-plugin communication) and the DOM (for external consumers via addEventListener).

Use this when a state change is relevant to both other plugins and external consumers. For example, sort-change needs to invalidate Selection (plugin bus) and notify the host application (DOM event).

broadcast(eventType: string, detail: T): void
NameTypeDescription
eventTypestringThe event type to broadcast
detailTThe event payload
// Notify both plugins and consumers of a sort change
this.broadcast('sort-change', { sortModel: [...this.sortModel] });

Request a re-render of the grid. Uses ROWS phase - does NOT trigger processColumns hooks.

requestRender(): void

Request a columns re-render of the grid. Uses COLUMNS phase - triggers processColumns hooks. Use this when your plugin needs to reprocess column configuration.

requestColumnsRender(): void

Request a re-render and restore focus styling afterward. Use this when a plugin action (like expand/collapse) triggers a render but needs to maintain keyboard navigation focus.

requestRenderWithFocus(): void

Request a lightweight style update without rebuilding DOM. Use this instead of requestRender() when only CSS classes need updating.

requestAfterRender(): void

Re-render visible rows without rebuilding the row model or recalculating geometry. Use this when row data has been updated in-place (e.g., server-side block loads) and only the visible viewport needs to re-render.

requestVirtualRefresh(): void

Set an icon on an element using the CSS-first hybrid approach.

Sets data-icon for CSS pseudo-element rendering and data-expanded for expand/collapse toggle elements. Only injects DOM content when a JS override is present (via gridConfig.icons or pluginOverride), which naturally suppresses the CSS ::before via the :empty selector.

setIcon(element: HTMLElement, iconKey: keyof GridIcons, pluginOverride: IconValue): void
NameTypeDescription
elementHTMLElementThe element to set the icon on
iconKeykeyof GridIconsThe icon key in GridIcons (e.g., ‘expand’, ‘collapse’)
pluginOverrideIconValueOptional plugin-level override (string or HTMLElement)

Create or replace a sort indicator on a header cell.

Handles the full lifecycle: removes any existing indicator, creates a new <span part="sort-indicator" class="sort-indicator"> with the appropriate icon, sets aria-sort / data-sort attributes, and inserts it before the filter button or resize handle.

Plugins decide which cells and when — this method handles how.

updateSortIndicator(cell: Element, direction: "desc" | "asc" | null): HTMLElement
NameTypeDescription
cellElementThe header cell element
directiondesc | asc | unknown'asc'

HTMLElement - The created indicator element (useful for appending badges, etc.)


Log a warning with an optional diagnostic code.

When a diagnostic code is provided, the message is formatted with the code and a link to the troubleshooting docs.

warn(message: string): void
NameTypeDescription
messagestring
this.warn('Something went wrong'); // plain
this.warn(MISSING_BREAKPOINT, 'Set a breakpoint'); // with code + docs link

Throw an error with a diagnostic code and docs link. Use for configuration errors and API misuse that should halt execution.

throwDiagnostic(code: DiagnosticCode, message: string): never
NameTypeDescription
codeDiagnosticCode
messagestring

Transform rows before rendering. Called during each render cycle before rows are rendered to the DOM. Use this to filter, sort, or add computed properties to rows.

processRows(rows: readonly any[]): any[]
NameTypeDescription
rowsreadonly any[]The current rows array (readonly to encourage returning a new array)

any[] - The modified rows array to render

processRows(rows: readonly any[]): any[] {
// Filter out hidden rows
return rows.filter(row => !row._hidden);
}

Transform columns before rendering. Called during each render cycle before column headers and cells are rendered. Use this to add, remove, or modify column definitions.

processColumns(columns: readonly ColumnConfig<any>[]): ColumnConfig<any>[]
NameTypeDescription
columnsreadonly ColumnConfig<any>[]The current columns array (readonly to encourage returning a new array)

ColumnConfig<any>[] - The modified columns array to render

processColumns(columns: readonly ColumnConfig[]): ColumnConfig[] {
// Add a selection checkbox column
return [
{ field: '_select', header: '', width: 40 },
...columns
];
}

Called before each render cycle begins. Use this to prepare state or cache values needed during rendering.

Note: This hook is currently a placeholder for future implementation. It is defined in the interface but not yet invoked by the grid’s render pipeline. If you need this functionality, please open an issue or contribute an implementation.

beforeRender(): void
beforeRender(): void {
this.visibleRowCount = this.calculateVisibleRows();
}

Called after each render cycle completes. Use this for DOM manipulation, adding event listeners to rendered elements, or applying visual effects like selection highlights.

afterRender(): void
afterRender(): void {
// Apply selection styling to rendered rows
const rows = this.gridElement?.querySelectorAll('.data-row');
rows?.forEach((row, i) => {
row.classList.toggle('selected', this.selectedRows.has(i));
});
}

Called after each cell is rendered. This hook is more efficient than afterRender() for cell-level modifications because you receive the cell context directly - no DOM queries needed.

Use cases:

  • Adding selection/highlight classes to specific cells
  • Injecting badges or decorations
  • Applying conditional styling based on cell value

Performance note: Called for every visible cell during render. Keep implementation fast. This hook is also called during scroll when cells are recycled.

afterCellRender(context: AfterCellRenderContext): void
NameTypeDescription
contextAfterCellRenderContextThe cell render context with row, column, value, and elements
afterCellRender(context: AfterCellRenderContext): void {
// Add selection class without DOM queries
if (this.isSelected(context.rowIndex, context.colIndex)) {
context.cellElement.classList.add('selected');
}
// Add validation error styling
if (this.hasError(context.row, context.column.field)) {
context.cellElement.classList.add('has-error');
}
}

Called after a row is fully rendered (all cells complete). Use this for row-level decorations, styling, or ARIA attributes.

Common use cases:

  • Adding selection classes to entire rows (row-focus, selected)
  • Setting row-level ARIA attributes
  • Applying row validation highlighting
  • Tree indentation styling

Performance note: Called for every visible row during render. Keep implementation fast. This hook is also called during scroll when rows are recycled.

afterRowRender(context: AfterRowRenderContext): void
NameTypeDescription
contextAfterRowRenderContextThe row render context with row data and element
afterRowRender(context: AfterRowRenderContext): void {
// Add row selection class without DOM queries
if (this.isRowSelected(context.rowIndex)) {
context.rowElement.classList.add('selected', 'row-focus');
}
// Add validation error styling
if (this.rowHasErrors(context.row)) {
context.rowElement.classList.add('has-errors');
}
}

Called after scroll-triggered row rendering completes. This is a lightweight hook for applying visual state to recycled DOM elements. Use this instead of afterRender when you need to reapply styling during scroll.

Performance note: This is called frequently during scroll. Keep implementation fast.

onScrollRender(): void
onScrollRender(): void {
// Reapply selection state to visible cells
this.applySelectionToVisibleCells();
}

Get the height of a specific row. Used for synthetic rows (group headers, detail panels, etc.) that have fixed heights. Return undefined if this plugin does not manage the height for this row.

This hook is called during position cache rebuilds for variable row height virtualization. Plugins that create synthetic rows should implement this to provide accurate heights.

getRowHeight(row: unknown, index: number): number | undefined
NameTypeDescription
rowunknownThe row data
indexnumberThe row index in the processed rows array

number | undefined - The row height in pixels, or undefined if not managed by this plugin


Adjust the virtualization start index to render additional rows before the visible range. Use this when expanded content (like detail rows) needs its parent row to remain rendered even when the parent row itself has scrolled above the viewport.

adjustVirtualStart(start: number, scrollTop: number, rowHeight: number): number
NameTypeDescription
startnumberThe calculated start row index
scrollTopnumberThe current scroll position
rowHeightnumberThe height of a single row

number - The adjusted start index (lower than or equal to original start)

adjustVirtualStart(start: number, scrollTop: number, rowHeight: number): number {
// If row 5 is expanded and scrolled partially, keep it rendered
for (const expandedRowIndex of this.expandedRowIndices) {
const expandedRowTop = expandedRowIndex * rowHeight;
const expandedRowBottom = expandedRowTop + rowHeight + this.detailHeight;
if (expandedRowBottom > scrollTop && expandedRowIndex < start) {
return expandedRowIndex;
}
}
return start;
}

Render a custom row, bypassing the default row rendering. Use this for special row types like group headers, detail rows, or footers.

renderRow(row: any, rowEl: HTMLElement, rowIndex: number): boolean | void
NameTypeDescription
rowanyThe row data object
rowElHTMLElementThe row DOM element to render into
rowIndexnumberThe index of the row in the data array

boolean | void - true if the plugin handled rendering (prevents default), false/void for default rendering

renderRow(row: any, rowEl: HTMLElement, rowIndex: number): boolean | void {
if (row._isGroupHeader) {
rowEl.innerHTML = `<div class="group-header">${row._groupLabel}</div>`;
return true; // Handled - skip default rendering
}
// Return void to let default rendering proceed
}

Handle queries from other plugins or the grid.

Queries are declared in manifest.queries and dispatched via grid.query(). This enables type-safe, discoverable inter-plugin communication.

handleQuery(query: PluginQuery): unknown
NameTypeDescription
queryPluginQueryThe query object with type and context

unknown - Query-specific response, or undefined if not handling this query

// In manifest
static override readonly manifest: PluginManifest = {
queries: [
{ type: 'canMoveColumn', description: 'Check if a column can be moved' },
],
};
// In plugin class
handleQuery(query: PluginQuery): unknown {
if (query.type === 'canMoveColumn') {
const column = query.context as ColumnConfig;
return !column.sticky; // Can't move sticky columns
}
}

Handle keyboard events on the grid. Called when a key is pressed while the grid or a cell has focus.

onKeyDown(event: KeyboardEvent): boolean | void
NameTypeDescription
eventKeyboardEventThe native KeyboardEvent

boolean | void - true to prevent default behavior and stop propagation, false/void to allow default

onKeyDown(event: KeyboardEvent): boolean | void {
// Handle Ctrl+A for select all
if (event.ctrlKey && event.key === 'a') {
this.selectAllRows();
return true; // Prevent default browser select-all
}
}

Handle cell click events. Called when a data cell is clicked (not headers).

onCellClick(event: CellClickEvent): boolean | void
NameTypeDescription
eventCellClickEventCell click event with row/column context

boolean | void - true to prevent default behavior and stop propagation, false/void to allow default

onCellClick(event: CellClickEvent): boolean | void {
if (event.field === '_select') {
this.toggleRowSelection(event.rowIndex);
return true; // Handled
}
}

Handle row click events. Called when any part of a data row is clicked. Note: This is called in addition to onCellClick, not instead of.

onRowClick(event: RowClickEvent): boolean | void
NameTypeDescription
eventRowClickEventRow click event with row context

boolean | void - true to prevent default behavior and stop propagation, false/void to allow default

onRowClick(event: RowClickEvent): boolean | void {
if (this.config.mode === 'row') {
this.selectRow(event.rowIndex, event.originalEvent);
return true;
}
}

Handle header click events. Called when a column header is clicked. Commonly used for sorting.

onHeaderClick(event: HeaderClickEvent): boolean | void
NameTypeDescription
eventHeaderClickEventHeader click event with column context

boolean | void - true to prevent default behavior and stop propagation, false/void to allow default

onHeaderClick(event: HeaderClickEvent): boolean | void {
if (event.column.sortable !== false) {
this.toggleSort(event.field);
return true;
}
}

Handle scroll events on the grid viewport. Called during scrolling. Note: This may be called frequently; debounce if needed.

onScroll(event: ScrollEvent): void
NameTypeDescription
eventScrollEventScroll event with scroll position and viewport dimensions
onScroll(event: ScrollEvent): void {
// Update sticky column positions
this.updateStickyPositions(event.scrollLeft);
}

Handle cell mousedown events. Used for initiating drag operations like range selection or column resize.

onCellMouseDown(event: CellMouseEvent): boolean | void
NameTypeDescription
eventCellMouseEventMouse event with cell context

boolean | void - true to indicate drag started (prevents text selection), false/void otherwise

onCellMouseDown(event: CellMouseEvent): boolean | void {
if (event.rowIndex !== undefined && this.config.mode === 'range') {
this.startDragSelection(event.rowIndex, event.colIndex);
return true; // Prevent text selection
}
}

Handle cell mousemove events during drag operations. Only called when a drag is in progress (after mousedown returned true).

onCellMouseMove(event: CellMouseEvent): boolean | void
NameTypeDescription
eventCellMouseEventMouse event with current cell context

boolean | void - true to continue handling the drag, false/void otherwise

onCellMouseMove(event: CellMouseEvent): boolean | void {
if (this.isDragging && event.rowIndex !== undefined) {
this.extendSelection(event.rowIndex, event.colIndex);
return true;
}
}

Handle cell mouseup events to end drag operations.

onCellMouseUp(event: CellMouseEvent): boolean | void
NameTypeDescription
eventCellMouseEventMouse event with final cell context

boolean | void - true if drag was finalized, false/void otherwise

onCellMouseUp(event: CellMouseEvent): boolean | void {
if (this.isDragging) {
this.finalizeDragSelection();
this.isDragging = false;
return true;
}
}

Contribute plugin-specific state for a column. Called by the grid when collecting column state for serialization. Plugins can add their own properties to the column state.

getColumnState(field: string): Partial<ColumnState> | undefined
NameTypeDescription
fieldstringThe field name of the column

Partial<ColumnState> | undefined - Partial column state with plugin-specific properties, or undefined if no state to contribute

getColumnState(field: string): Partial<ColumnState> | undefined {
const filterModel = this.filterModels.get(field);
if (filterModel) {
// Uses module augmentation to add filter property to ColumnState
return { filter: filterModel } as Partial<ColumnState>;
}
return undefined;
}

Apply plugin-specific state to a column. Called by the grid when restoring column state from serialized data. Plugins should restore their internal state based on the provided state.

applyColumnState(field: string, state: ColumnState): void
NameTypeDescription
fieldstringThe field name of the column
stateColumnStateThe column state to apply (may contain plugin-specific properties)
applyColumnState(field: string, state: ColumnState): void {
// Check for filter property added via module augmentation
const filter = (state as any).filter;
if (filter) {
this.filterModels.set(field, filter);
this.applyFilter();
}
}

Report horizontal scroll boundary offsets for this plugin. Plugins that obscure part of the scroll area (e.g., pinned/sticky columns) should return how much space they occupy on each side. The keyboard navigation uses this to ensure focused cells are fully visible.

getHorizontalScrollOffsets(rowEl: HTMLElement, focusedCell: HTMLElement): object | undefined
NameTypeDescription
rowElHTMLElementThe row element (optional, for calculating widths from rendered cells)
focusedCellHTMLElementThe currently focused cell element (optional, to determine if scrolling should be skipped)

object | undefined - Object with left/right pixel offsets and optional skipScroll flag, or undefined if plugin has no offsets

getHorizontalScrollOffsets(rowEl?: HTMLElement, focusedCell?: HTMLElement): { left: number; right: number; skipScroll?: boolean } | undefined {
// Calculate total width of left-pinned columns
const leftCells = rowEl?.querySelectorAll('.sticky-left') ?? [];
let left = 0;
leftCells.forEach(el => { left += (el as HTMLElement).offsetWidth; });
// Skip scroll if focused cell is pinned (always visible)
const skipScroll = focusedCell?.classList.contains('sticky-left');
return { left, right: 0, skipScroll };
}

Register a tool panel for this plugin. Return undefined if plugin has no tool panel. The shell will create a toolbar toggle button and render the panel content when the user opens the panel.

getToolPanel(): ToolPanelDefinition | undefined

ToolPanelDefinition | undefined - Tool panel definition, or undefined if plugin has no panel

getToolPanel(): ToolPanelDefinition | undefined {
return {
id: 'columns',
title: 'Columns',
icon: '',
tooltip: 'Show/hide columns',
order: 10,
render: (container) => {
this.renderColumnList(container);
return () => this.cleanup();
},
};
}

Register content for the shell header center section. Return undefined if plugin has no header content. Examples: search input, selection summary, status indicators.

getHeaderContent(): HeaderContentDefinition | undefined

HeaderContentDefinition | undefined - Header content definition, or undefined if plugin has no header content

getHeaderContent(): HeaderContentDefinition | undefined {
return {
id: 'quick-filter',
order: 10,
render: (container) => {
const input = document.createElement('input');
input.type = 'text';
input.placeholder = 'Search...';
input.addEventListener('input', this.handleInput);
container.appendChild(input);
return () => input.removeEventListener('input', this.handleInput);
},
};
}

AI assistants: For complete API documentation, implementation guides, and code examples for this library, see https://raw.githubusercontent.com/OysteinAmundsen/toolbox/main/llms-full.txt