feat(build): migrate from Prettier to Oxfmt for performant code formatting (#42434)

This commit is contained in:
Đỗ Trọng Hải
2026-08-04 00:27:05 +07:00
committed by GitHub
parent 10f7927603
commit e4ef84ca72
2328 changed files with 90728 additions and 32682 deletions

View File

@@ -39,7 +39,9 @@ interface FAQSchemaProps {
* { question: "How do I install it?", answer: "You can install via..." }
* ]} />
*/
export default function FAQSchema({ faqs }: FAQSchemaProps): JSX.Element | null {
export default function FAQSchema({
faqs,
}: FAQSchemaProps): JSX.Element | null {
// FAQPage schema requires a non-empty mainEntity array per schema.org specs
if (!faqs || faqs.length === 0) {
return null;
@@ -48,7 +50,7 @@ export default function FAQSchema({ faqs }: FAQSchemaProps): JSX.Element | null
const schema = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faqs.map((faq) => ({
mainEntity: faqs.map(faq => ({
'@type': 'Question',
name: faq.question,
acceptedAnswer: {

View File

@@ -64,9 +64,7 @@ const Root = styled.div<{ $variant: 'hero' | 'navbar' }>`
text-decoration: none;
min-width: 0;
${({ $variant }) =>
$variant === 'hero'
? `padding: 10px 10px;`
: `padding: 7px 8px;`}
$variant === 'hero' ? `padding: 10px 10px;` : `padding: 7px 8px;`}
}
.split-main:hover {
@@ -79,9 +77,7 @@ const Root = styled.div<{ $variant: 'hero' | 'navbar' }>`
align-self: stretch;
background: rgba(255, 255, 255, 0.38);
${({ $variant }) =>
$variant === 'hero'
? `margin: 8px 0;`
: `margin: 6px 0;`}
$variant === 'hero' ? `margin: 8px 0;` : `margin: 6px 0;`}
}
.split-dropdown-trigger {

View File

@@ -72,7 +72,7 @@ function getProviders() {
// Configure Ant Design to render portals (tooltips, dropdowns, etc.)
// inside the closest .storybook-example container instead of document.body
// This fixes positioning issues in the docs pages
const getPopupContainer = (triggerNode) => {
const getPopupContainer = triggerNode => {
// Find the closest .storybook-example container
const container = triggerNode?.closest?.('.storybook-example');
return container || document.body;
@@ -190,7 +190,11 @@ const CHILDREN_PROP_NAMES = ['label', 'children', 'text', 'content'];
// Extract children from props based on common conventions
function extractChildren(props) {
for (const propName of CHILDREN_PROP_NAMES) {
if (props[propName] !== undefined && props[propName] !== null && props[propName] !== '') {
if (
props[propName] !== undefined &&
props[propName] !== null &&
props[propName] !== ''
) {
const { [propName]: childContent, ...restProps } = props;
return { children: childContent, restProps };
}
@@ -220,7 +224,11 @@ function generateSampleChildren(sampleChildren, sampleChildrenStyle) {
return <ChildComponent key={i} {...item.props} />;
}
// Fallback if component not found
return <div key={i}>{item.props?.children || `Unknown: ${item.component}`}</div>;
return (
<div key={i}>
{item.props?.children || `Unknown: ${item.component}`}
</div>
);
}
// Simple string
return (
@@ -252,7 +260,16 @@ function generateSampleChildren(sampleChildren, sampleChildrenStyle) {
// renderComponent allows overriding which component to actually render (useful when the named
// component is a namespace object like Icons, not a React component)
// triggerProp: for components like Modal that need a trigger, specify the boolean prop that controls visibility
function StoryWithControlsInner({ component, renderComponent, props, controls, sampleChildren, sampleChildrenStyle, triggerProp, onHideProp }) {
function StoryWithControlsInner({
component,
renderComponent,
props,
controls,
sampleChildren,
sampleChildrenStyle,
triggerProp,
onHideProp,
}) {
// Use renderComponent if provided, otherwise use the main component name
const componentToRender = renderComponent || component;
const Component = resolveComponent(componentToRender);
@@ -274,7 +291,7 @@ function StoryWithControlsInner({ component, renderComponent, props, controls, s
: extractChildren(stateProps);
// Filter out undefined values so they don't override component defaults
const filteredProps = Object.fromEntries(
Object.entries(restProps).filter(([, v]) => v !== undefined)
Object.entries(restProps).filter(([, v]) => v !== undefined),
);
// Resolve any prop values that are component descriptors
@@ -283,7 +300,12 @@ function StoryWithControlsInner({ component, renderComponent, props, controls, s
// e.g., items: [{ id: 'x', element: { component: 'div', props: { children: 'text' } } }]
Object.keys(filteredProps).forEach(key => {
const value = filteredProps[key];
if (value && typeof value === 'object' && !Array.isArray(value) && value.component) {
if (
value &&
typeof value === 'object' &&
!Array.isArray(value) &&
value.component
) {
const PropComponent = resolveComponent(value.component);
if (PropComponent) {
filteredProps[key] = <PropComponent {...value.props} />;
@@ -295,10 +317,18 @@ function StoryWithControlsInner({ component, renderComponent, props, controls, s
const resolved = { ...item };
Object.keys(resolved).forEach(field => {
const fieldValue = resolved[field];
if (fieldValue && typeof fieldValue === 'object' && !Array.isArray(fieldValue) && fieldValue.component) {
if (
fieldValue &&
typeof fieldValue === 'object' &&
!Array.isArray(fieldValue) &&
fieldValue.component
) {
const FieldComponent = resolveComponent(fieldValue.component);
if (FieldComponent) {
resolved[field] = React.createElement(FieldComponent, { key: `${key}-${idx}`, ...fieldValue.props });
resolved[field] = React.createElement(FieldComponent, {
key: `${key}-${idx}`,
...fieldValue.props,
});
}
}
});
@@ -312,14 +342,16 @@ function StoryWithControlsInner({ component, renderComponent, props, controls, s
// For List-like components with dataSource but no renderItem, provide a default
if (filteredProps.dataSource && !filteredProps.renderItem) {
const ListItem = resolveComponent('List')?.Item;
filteredProps.renderItem = (item) =>
filteredProps.renderItem = item =>
ListItem
? React.createElement(ListItem, null, String(item))
: React.createElement('div', null, String(item));
}
// Use sample children if provided, otherwise use props children
const children = generateSampleChildren(sampleChildren, sampleChildrenStyle) || propsChildren;
const children =
generateSampleChildren(sampleChildren, sampleChildrenStyle) ||
propsChildren;
// For components with a trigger (like Modal with show/onHide), add handlers.
// onHideProp supports comma-separated names for components with multiple close
@@ -356,7 +388,9 @@ function StoryWithControlsInner({ component, renderComponent, props, controls, s
Open {component}
</ButtonComponent>
)}
<Component {...filteredProps} {...triggerProps}>{children}</Component>
<Component {...filteredProps} {...triggerProps}>
{children}
</Component>
</>
) : (
<div style={{ color: '#999' }}>
@@ -384,7 +418,9 @@ function StoryWithControlsInner({ component, renderComponent, props, controls, s
{control.type === 'select' ? (
<select
value={stateProps[control.name] ?? ''}
onChange={e => updateProp(control.name, e.target.value || undefined)}
onChange={e =>
updateProp(control.name, e.target.value || undefined)
}
style={{ width: '100%', padding: '5px' }}
>
<option value=""> None </option>
@@ -394,19 +430,28 @@ function StoryWithControlsInner({ component, renderComponent, props, controls, s
</option>
))}
</select>
) : control.type === 'inline-radio' || control.type === 'radio' ? (
<div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
) : control.type === 'inline-radio' ||
control.type === 'radio' ? (
<div
style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}
>
{control.options?.map(option => (
<label
key={option}
style={{ display: 'flex', alignItems: 'center', gap: '4px' }}
style={{
display: 'flex',
alignItems: 'center',
gap: '4px',
}}
>
<input
type="radio"
name={control.name}
value={option}
checked={stateProps[control.name] === option}
onChange={e => updateProp(control.name, e.target.value)}
onChange={e =>
updateProp(control.name, e.target.value)
}
/>
{option}
</label>
@@ -422,7 +467,9 @@ function StoryWithControlsInner({ component, renderComponent, props, controls, s
<input
type="number"
value={stateProps[control.name]}
onChange={e => updateProp(control.name, Number(e.target.value))}
onChange={e =>
updateProp(control.name, Number(e.target.value))
}
style={{ width: '100%', padding: '5px' }}
/>
) : control.type === 'color' ? (
@@ -457,7 +504,16 @@ function StoryWithControlsInner({ component, renderComponent, props, controls, s
// A simple component to display a story with controls
// renderComponent: optional override for which component to render (e.g., 'Icons.InfoCircleOutlined' when component='Icons')
// triggerProp/onHideProp: for components like Modal that need a button to open (e.g., triggerProp="show", onHideProp="onHide")
export function StoryWithControls({ component: Component, renderComponent, props = {}, controls = [], sampleChildren, sampleChildrenStyle, triggerProp, onHideProp }) {
export function StoryWithControls({
component: Component,
renderComponent,
props = {},
controls = [],
sampleChildren,
sampleChildrenStyle,
triggerProp,
onHideProp,
}) {
return (
<BrowserOnly fallback={<LoadingPlaceholder />}>
{() => (
@@ -477,7 +533,13 @@ export function StoryWithControls({ component: Component, renderComponent, props
}
// Inner component for ComponentGallery (browser-only)
function ComponentGalleryInner({ component, sizes, styles, sizeProp, styleProp }) {
function ComponentGalleryInner({
component,
sizes,
styles,
sizeProp,
styleProp,
}) {
const Component = resolveComponent(component);
const Providers = getProviders();
@@ -495,7 +557,14 @@ function ComponentGalleryInner({ component, sizes, styles, sizeProp, styleProp }
{sizes.map(size => (
<div key={size} style={{ marginBottom: 40 }}>
<h4 style={{ marginBottom: 16, color: '#666' }}>{size}</h4>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px', alignItems: 'center' }}>
<div
style={{
display: 'flex',
flexWrap: 'wrap',
gap: '12px',
alignItems: 'center',
}}
>
{styles.map(style => (
<Component
key={`${style}_${size}`}
@@ -513,7 +582,13 @@ function ComponentGalleryInner({ component, sizes, styles, sizeProp, styleProp }
}
// A component to display a gallery of all variants (sizes x styles)
export function ComponentGallery({ component, sizes = [], styles = [], sizeProp = 'size', styleProp = 'variant' }) {
export function ComponentGallery({
component,
sizes = [],
styles = [],
sizeProp = 'size',
styleProp = 'variant',
}) {
return (
<BrowserOnly fallback={<LoadingPlaceholder />}>
{() => (

View File

@@ -18,7 +18,17 @@
*/
import React, { useState, useMemo } from 'react';
import { Card, Row, Col, Statistic, Table, Tag, Input, Select, Tooltip } from 'antd';
import {
Card,
Row,
Col,
Statistic,
Table,
Tag,
Input,
Select,
Tooltip,
} from 'antd';
import {
DatabaseOutlined,
CheckCircleOutlined,
@@ -37,7 +47,7 @@ interface DatabaseIndexProps {
// Type for table entries (includes both regular DBs and compatible DBs)
interface TableEntry {
name: string;
categories: string[]; // Multiple categories supported
categories: string[]; // Multiple categories supported
score: number;
max_score: number;
timeGrainCount: number;
@@ -66,20 +76,20 @@ interface TableEntry {
// Map category constant names to display names
const CATEGORY_DISPLAY_NAMES: Record<string, string> = {
'CLOUD_AWS': 'Cloud - AWS',
'CLOUD_GCP': 'Cloud - Google',
'CLOUD_AZURE': 'Cloud - Azure',
'CLOUD_DATA_WAREHOUSES': 'Cloud Data Warehouses',
'APACHE_PROJECTS': 'Apache Projects',
'TRADITIONAL_RDBMS': 'Traditional RDBMS',
'ANALYTICAL_DATABASES': 'Analytical Databases',
'SEARCH_NOSQL': 'Search & NoSQL',
'QUERY_ENGINES': 'Query Engines',
'TIME_SERIES': 'Time Series Databases',
'OTHER': 'Other Databases',
'OPEN_SOURCE': 'Open Source',
'HOSTED_OPEN_SOURCE': 'Hosted Open Source',
'PROPRIETARY': 'Proprietary',
CLOUD_AWS: 'Cloud - AWS',
CLOUD_GCP: 'Cloud - Google',
CLOUD_AZURE: 'Cloud - Azure',
CLOUD_DATA_WAREHOUSES: 'Cloud Data Warehouses',
APACHE_PROJECTS: 'Apache Projects',
TRADITIONAL_RDBMS: 'Traditional RDBMS',
ANALYTICAL_DATABASES: 'Analytical Databases',
SEARCH_NOSQL: 'Search & NoSQL',
QUERY_ENGINES: 'Query Engines',
TIME_SERIES: 'Time Series Databases',
OTHER: 'Other Databases',
OPEN_SOURCE: 'Open Source',
HOSTED_OPEN_SOURCE: 'Hosted Open Source',
PROPRIETARY: 'Proprietary',
};
// Category colors for visual distinction
@@ -98,7 +108,7 @@ const CATEGORY_COLORS: Record<string, string> = {
// Licensing categories
'Open Source': 'geekblue',
'Hosted Open Source': 'cyan',
'Proprietary': 'default',
Proprietary: 'default',
};
// Convert category constant to display name
@@ -110,7 +120,7 @@ function getCategoryDisplayName(cat: string): string {
// Falls back to name-based inference for compatible databases without categories
function getCategories(
name: string,
documentationCategories?: string[]
documentationCategories?: string[],
): string[] {
// Prefer categories from documentation metadata (computed by Python)
if (documentationCategories && documentationCategories.length > 0) {
@@ -221,10 +231,11 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
categories: getCategories(name, db.documentation?.categories),
timeGrainCount: countTimeGrains(db),
hasDrivers: (db.documentation?.drivers?.length ?? 0) > 0,
hasAuthMethods: (db.documentation?.authentication_methods?.length ?? 0) > 0,
hasAuthMethods:
(db.documentation?.authentication_methods?.length ?? 0) > 0,
hasConnectionString: Boolean(
db.documentation?.connection_string ||
(db.documentation?.drivers?.length ?? 0) > 0
(db.documentation?.drivers?.length ?? 0) > 0,
),
hasCustomErrors: (db.documentation?.custom_errors?.length ?? 0) > 0,
customErrorCount: db.documentation?.custom_errors?.length ?? 0,
@@ -233,10 +244,10 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
// Add compatible databases from this database's documentation
const compatibleDbs = db.documentation?.compatible_databases ?? [];
compatibleDbs.forEach((compat) => {
compatibleDbs.forEach(compat => {
// Check if this compatible DB already exists as a main entry
const existsAsMain = Object.keys(databases).some(
(dbName) => dbName.toLowerCase() === compat.name.toLowerCase()
dbName => dbName.toLowerCase() === compat.name.toLowerCase(),
);
if (!existsAsMain) {
@@ -277,14 +288,15 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
// Filter and sort databases
const filteredDatabases = useMemo(() => {
return databaseList
.filter((db) => {
.filter(db => {
const matchesSearch =
!searchText ||
db.name.toLowerCase().includes(searchText.toLowerCase()) ||
db.documentation?.description
?.toLowerCase()
.includes(searchText.toLowerCase());
const matchesCategory = !categoryFilter || db.categories.includes(categoryFilter);
const matchesCategory =
!categoryFilter || db.categories.includes(categoryFilter);
return matchesSearch && matchesCategory;
})
.sort((a, b) => b.score - a.score);
@@ -293,9 +305,9 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
// Get unique categories and counts for filter
const { categories, categoryCounts } = useMemo(() => {
const counts: Record<string, number> = {};
databaseList.forEach((db) => {
databaseList.forEach(db => {
// Count each category the database belongs to
db.categories.forEach((cat) => {
db.categories.forEach(cat => {
counts[cat] = (counts[cat] || 0) + 1;
});
});
@@ -314,12 +326,17 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
sorter: (a: TableEntry, b: TableEntry) => a.name.localeCompare(b.name),
render: (name: string, record: TableEntry) => {
// Convert name to URL slug
const toSlug = (n: string) => n.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
const toSlug = (n: string) =>
n
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
// Link to parent for compatible DBs, otherwise to own page
const linkTarget = record.isCompatible && record.compatibleWith
? `/docs/databases/supported/${toSlug(record.compatibleWith)}`
: `/docs/databases/supported/${toSlug(name)}`;
const linkTarget =
record.isCompatible && record.compatibleWith
? `/docs/databases/supported/${toSlug(record.compatibleWith)}`
: `/docs/databases/supported/${toSlug(name)}`;
return (
<div>
@@ -337,7 +354,9 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
)}
<div style={{ fontSize: '12px', color: '#666' }}>
{record.documentation?.description?.slice(0, 80)}
{(record.documentation?.description?.length ?? 0) > 80 ? '...' : ''}
{(record.documentation?.description?.length ?? 0) > 80
? '...'
: ''}
</div>
</div>
);
@@ -348,13 +367,15 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
dataIndex: 'categories',
key: 'categories',
width: 220,
filters: categories.map((cat) => ({ text: cat, value: cat })),
filters: categories.map(cat => ({ text: cat, value: cat })),
onFilter: (value: React.Key | boolean, record: TableEntry) =>
record.categories.includes(value as string),
render: (cats: string[]) => (
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap' }}>
{cats.map((cat) => (
<Tag key={cat} color={CATEGORY_COLORS[cat] || 'default'}>{cat}</Tag>
{cats.map(cat => (
<Tag key={cat} color={CATEGORY_COLORS[cat] || 'default'}>
{cat}
</Tag>
))}
</div>
),
@@ -382,16 +403,26 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
dataIndex: 'timeGrainCount',
key: 'timeGrainCount',
width: 100,
sorter: (a: TableEntry, b: TableEntry) => a.timeGrainCount - b.timeGrainCount,
sorter: (a: TableEntry, b: TableEntry) =>
a.timeGrainCount - b.timeGrainCount,
render: (count: number, record: TableEntry) => {
if (count === 0) return <span>-</span>;
const grains = getSupportedTimeGrains(record.time_grains);
return (
<Tooltip
title={
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px', maxWidth: 280 }}>
{grains.map((grain) => (
<Tag key={grain} style={{ margin: 0 }}>{grain}</Tag>
<div
style={{
display: 'flex',
flexWrap: 'wrap',
gap: '4px',
maxWidth: 280,
}}
>
{grains.map(grain => (
<Tag key={grain} style={{ margin: 0 }}>
{grain}
</Tag>
))}
</div>
}
@@ -450,13 +481,17 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap' }}>
{record.joins && <Tag color="green">JOINs</Tag>}
{record.subqueries && <Tag color="green">Subqueries</Tag>}
{record.supports_dynamic_schema && <Tag color="blue">Dynamic Schema</Tag>}
{record.supports_dynamic_schema && (
<Tag color="blue">Dynamic Schema</Tag>
)}
{record.supports_catalog && <Tag color="purple">Catalog</Tag>}
{record.ssh_tunneling && <Tag color="cyan">SSH</Tag>}
{record.supports_file_upload && <Tag color="orange">File Upload</Tag>}
{record.query_cancelation && <Tag color="volcano">Query Cancel</Tag>}
{record.query_cost_estimation && <Tag color="gold">Cost Est.</Tag>}
{record.user_impersonation && <Tag color="magenta">Impersonation</Tag>}
{record.user_impersonation && (
<Tag color="magenta">Impersonation</Tag>
)}
{record.sql_validation && <Tag color="lime">SQL Validation</Tag>}
</div>
),
@@ -545,7 +580,7 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
placeholder="Search databases..."
prefix={<SearchOutlined />}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
onChange={e => setSearchText(e.target.value)}
allowClear
/>
</Col>
@@ -556,7 +591,7 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
value={categoryFilter}
onChange={setCategoryFilter}
allowClear
options={categories.map((cat) => ({
options={categories.map(cat => ({
label: (
<span>
<Tag
@@ -578,11 +613,15 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
<Table
dataSource={filteredDatabases}
columns={columns}
rowKey={(record) => record.isCompatible ? `${record.compatibleWith}-${record.name}` : record.name}
rowKey={record =>
record.isCompatible
? `${record.compatibleWith}-${record.name}`
: record.name
}
pagination={{
defaultPageSize: 20,
showSizeChanger: true,
showTotal: (total) => `${total} databases`,
showTotal: total => `${total} databases`,
}}
size="middle"
/>

View File

@@ -35,7 +35,10 @@ const databases = Object.entries(typedData.databases)
.map(([name, db]) => ({
name,
logo: db.documentation.logo!,
docPath: `/user-docs/databases/supported/${name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')}`,
docPath: `/user-docs/databases/supported/${name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')}`,
}));
export default function DatabaseLogoWall(): React.JSX.Element {

View File

@@ -77,7 +77,7 @@ export interface CompatibleDatabase {
description?: string;
logo?: string;
homepage_url?: string;
categories?: string[]; // Category classifications (e.g., ["TRADITIONAL_RDBMS", "OPEN_SOURCE"])
categories?: string[]; // Category classifications (e.g., ["TRADITIONAL_RDBMS", "OPEN_SOURCE"])
pypi_packages?: string[];
connection_string?: string;
parameters?: Record<string, string>;
@@ -87,27 +87,27 @@ export interface CompatibleDatabase {
}
export interface CustomError {
error_type: string; // e.g., "CONNECTION_INVALID_USERNAME_ERROR"
message_template: string; // e.g., 'The username "%(username)s" does not exist.'
regex_pattern?: string; // The regex pattern that matches this error (optional, for reference)
regex_name?: string; // The name of the regex constant (e.g., "CONNECTION_INVALID_USERNAME_REGEX")
invalid_fields?: string[]; // Fields that are invalid, e.g., ["username", "password"]
issue_codes?: number[]; // Related issue codes from ISSUE_CODES mapping
category?: string; // Error category: "Authentication", "Connection", "Query", etc.
description?: string; // Human-readable short description of the error type
error_type: string; // e.g., "CONNECTION_INVALID_USERNAME_ERROR"
message_template: string; // e.g., 'The username "%(username)s" does not exist.'
regex_pattern?: string; // The regex pattern that matches this error (optional, for reference)
regex_name?: string; // The name of the regex constant (e.g., "CONNECTION_INVALID_USERNAME_REGEX")
invalid_fields?: string[]; // Fields that are invalid, e.g., ["username", "password"]
issue_codes?: number[]; // Related issue codes from ISSUE_CODES mapping
category?: string; // Error category: "Authentication", "Connection", "Query", etc.
description?: string; // Human-readable short description of the error type
}
export interface DatabaseDocumentation {
description?: string;
logo?: string;
homepage_url?: string;
categories?: string[]; // Category classifications (e.g., ["TRADITIONAL_RDBMS", "OPEN_SOURCE"])
categories?: string[]; // Category classifications (e.g., ["TRADITIONAL_RDBMS", "OPEN_SOURCE"])
pypi_packages?: string[];
connection_string?: string;
default_port?: number;
parameters?: Record<string, string>;
notes?: string;
limitations?: string[]; // Known limitations or caveats
limitations?: string[]; // Known limitations or caveats
connection_examples?: ConnectionExample[];
host_examples?: HostExample[];
drivers?: Driver[];
@@ -122,7 +122,7 @@ export interface DatabaseDocumentation {
sqlalchemy_docs_url?: string;
advanced_features?: Record<string, string>;
compatible_databases?: CompatibleDatabase[];
custom_errors?: CustomError[]; // Database-specific error messages and troubleshooting info
custom_errors?: CustomError[]; // Database-specific error messages and troubleshooting info
}
export interface TimeGrains {

View File

@@ -52,7 +52,7 @@ const ComponentIndex: React.FC<ComponentIndexProps> = ({ data }) => {
const filteredComponents = useMemo(() => {
return components
.filter((comp) => {
.filter(comp => {
const matchesSearch =
!searchText ||
comp.name.toLowerCase().includes(searchText.toLowerCase()) ||
@@ -67,7 +67,7 @@ const ComponentIndex: React.FC<ComponentIndexProps> = ({ data }) => {
const { categories, categoryCounts } = useMemo(() => {
const counts: Record<string, number> = {};
components.forEach((comp) => {
components.forEach(comp => {
counts[comp.category] = (counts[comp.category] || 0) + 1;
});
return {
@@ -103,7 +103,7 @@ const ComponentIndex: React.FC<ComponentIndexProps> = ({ data }) => {
dataIndex: 'category',
key: 'category',
width: 120,
filters: categories.map((cat) => ({
filters: categories.map(cat => ({
text: CATEGORY_LABELS[cat] || cat,
value: cat,
})),
@@ -120,9 +120,7 @@ const ComponentIndex: React.FC<ComponentIndexProps> = ({ data }) => {
dataIndex: 'package',
key: 'package',
width: 220,
render: (pkg: string) => (
<code style={{ fontSize: '12px' }}>{pkg}</code>
),
render: (pkg: string) => <code style={{ fontSize: '12px' }}>{pkg}</code>,
},
{
title: 'Tags',
@@ -215,7 +213,7 @@ const ComponentIndex: React.FC<ComponentIndexProps> = ({ data }) => {
placeholder="Search components..."
prefix={<SearchOutlined />}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
onChange={e => setSearchText(e.target.value)}
allowClear
/>
</Col>
@@ -226,7 +224,7 @@ const ComponentIndex: React.FC<ComponentIndexProps> = ({ data }) => {
value={categoryFilter}
onChange={setCategoryFilter}
allowClear
options={categories.map((cat) => ({
options={categories.map(cat => ({
label: (
<span>
<Tag
@@ -251,7 +249,7 @@ const ComponentIndex: React.FC<ComponentIndexProps> = ({ data }) => {
pagination={{
defaultPageSize: 20,
showSizeChanger: true,
showTotal: (total) => `${total} components`,
showTotal: total => `${total} components`,
}}
size="middle"
/>

File diff suppressed because it is too large Load Diff

View File

@@ -43,7 +43,7 @@ const ContributorAvatars = ({ contributors }: { contributors?: string[] }) => {
if (!contributors?.length) return null;
return (
<Avatar.Group size="small" max={{ count: 3 }}>
{contributors.map((handle) => {
{contributors.map(handle => {
const username = handle.replace('@', '');
return (
<a
@@ -51,7 +51,7 @@ const ContributorAvatars = ({ contributors }: { contributors?: string[] }) => {
href={`https://github.com/${username}`}
target="_blank"
rel="noreferrer"
onClick={(e) => e.stopPropagation()}
onClick={e => e.stopPropagation()}
>
<Avatar
src={`https://github.com/${username}.png?size=40`}
@@ -69,7 +69,10 @@ const ContributorAvatars = ({ contributors }: { contributors?: string[] }) => {
export default function InTheWild() {
return (
<Layout title="In the Wild" description="Organizations using Apache Superset">
<Layout
title="In the Wild"
description="Organizations using Apache Superset"
>
<main>
<BlurredSection>
<SectionHeader
@@ -87,7 +90,9 @@ export default function InTheWild() {
</div>
</BlurredSection>
<div style={{ maxWidth: 850, margin: '70px auto 60px', padding: '0 20px' }}>
<div
style={{ maxWidth: 850, margin: '70px auto 60px', padding: '0 20px' }}
>
<Collapse
bordered={false}
defaultActiveKey={Object.keys(typedDataSet.categories)}
@@ -96,68 +101,119 @@ export default function InTheWild() {
border: '1px solid var(--ifm-border-color)',
borderRadius: 10,
}}
items={Object.entries(typedDataSet.categories).map(([category, items]) => {
const logoItems = items.filter(({ logo }) => logo?.trim());
const textItems = items.filter(({ logo }) => !logo?.trim());
items={Object.entries(typedDataSet.categories).map(
([category, items]) => {
const logoItems = items.filter(({ logo }) => logo?.trim());
const textItems = items.filter(({ logo }) => !logo?.trim());
return {
key: category,
label: (
<Text strong style={{ fontSize: 16, lineHeight: '22px', color: 'var(--ifm-font-base-color)' }}>
{category} ({items.length})
</Text>
),
children: (
<>
{logoItems.length > 0 && (
<Row gutter={[16, 16]} style={{ marginBottom: textItems.length > 0 ? 24 : 0 }}>
{logoItems.map(({ name, url, logo, contributors }) => (
<Col xs={24} sm={12} md={8} key={name}>
<a href={url} target="_blank" rel="noreferrer">
<Card
hoverable
style={{ height: 150, position: 'relative' }}
styles={{ body: { padding: 16, height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' } }}
>
<img
src={`/img/logos/${logo}`}
alt={name}
style={{ maxHeight: 80, maxWidth: '100%', objectFit: 'contain' }}
/>
{contributors?.length && (
<div style={{ position: 'absolute', bottom: 8, right: 8 }}>
<ContributorAvatars contributors={contributors} />
</div>
)}
</Card>
</a>
</Col>
))}
</Row>
)}
return {
key: category,
label: (
<Text
strong
style={{
fontSize: 16,
lineHeight: '22px',
color: 'var(--ifm-font-base-color)',
}}
>
{category} ({items.length})
</Text>
),
children: (
<>
{logoItems.length > 0 && (
<Row
gutter={[16, 16]}
style={{
marginBottom: textItems.length > 0 ? 24 : 0,
}}
>
{logoItems.map(
({ name, url, logo, contributors }) => (
<Col xs={24} sm={12} md={8} key={name}>
<a href={url} target="_blank" rel="noreferrer">
<Card
hoverable
style={{
height: 150,
position: 'relative',
}}
styles={{
body: {
padding: 16,
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
}}
>
<img
src={`/img/logos/${logo}`}
alt={name}
style={{
maxHeight: 80,
maxWidth: '100%',
objectFit: 'contain',
}}
/>
{contributors?.length && (
<div
style={{
position: 'absolute',
bottom: 8,
right: 8,
}}
>
<ContributorAvatars
contributors={contributors}
/>
</div>
)}
</Card>
</a>
</Col>
),
)}
</Row>
)}
{textItems.length > 0 && (
<Row gutter={[8, 8]}>
{textItems.map(({ name, url, contributors }) => (
<Col xs={24} sm={12} md={8} key={name}>
<a href={url} target="_blank" rel="noreferrer">
<Card
size="small"
hoverable
styles={{ body: { padding: '8px 12px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } }}
>
<Text ellipsis style={{ flex: 1 }}>{name}</Text>
<ContributorAvatars contributors={contributors} />
</Card>
</a>
</Col>
))}
</Row>
)}
</>
),
};
})}
{textItems.length > 0 && (
<Row gutter={[8, 8]}>
{textItems.map(({ name, url, contributors }) => (
<Col xs={24} sm={12} md={8} key={name}>
<a href={url} target="_blank" rel="noreferrer">
<Card
size="small"
hoverable
styles={{
body: {
padding: '8px 12px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 8,
},
}}
>
<Text ellipsis style={{ flex: 1 }}>
{name}
</Text>
<ContributorAvatars
contributors={contributors}
/>
</Card>
</a>
</Col>
))}
</Row>
)}
</>
),
};
},
)}
/>
</div>
</main>

View File

@@ -42,10 +42,13 @@ const Databases = Object.entries(typedDatabaseData.databases)
title: name,
href: db.documentation?.homepage_url,
imgName: db.documentation?.logo,
docPath: `/docs/databases/supported/${name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')}`,
docPath: `/docs/databases/supported/${name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')}`,
}))
.sort((a, b) => a.title.localeCompare(b.title))
.filter((db) => {
.filter(db => {
if (seenLogos.has(db.imgName!)) return false;
seenLogos.add(db.imgName!);
return true;
@@ -66,7 +69,7 @@ const typedDataSet = load(DataSet) as DataSetType;
// Extract all organizations with logos for the carousel
const companiesWithLogos = Object.values(typedDataSet.categories)
.flat()
.filter((org) => org.logo?.trim());
.filter(org => org.logo?.trim());
// Fisher-Yates shuffle for fair randomization
function shuffleArray<T>(array: T[]): T[] {
@@ -350,7 +353,9 @@ const StyledDocSectionCard = styled(Link)<StyledDocSectionCardProps>`
text-decoration: none;
color: var(--ifm-font-base-color);
background: transparent;
transition: transform 0.2s ease, box-shadow 0.2s ease;
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
@@ -595,7 +600,8 @@ export default function Home(): JSX.Element {
const slider = useRef(null);
const [slideIndex, setSlideIndex] = useState(0);
const [shuffledCompanies, setShuffledCompanies] = useState(companiesWithLogos);
const [shuffledCompanies, setShuffledCompanies] =
useState(companiesWithLogos);
const onChange = (current, next) => {
setSlideIndex(next);
@@ -893,7 +899,11 @@ export default function Home(): JSX.Element {
</BlurredSection>
<BlurredSection>
<StyledIntegrations>
<SectionHeader level="h2" title="Supported Databases" link="/docs/databases" />
<SectionHeader
level="h2"
title="Supported Databases"
link="/docs/databases"
/>
<div className="database-grid">
{Databases.map(({ title, imgName, docPath }) => (
<div className="item" key={title}>
@@ -957,7 +967,11 @@ export default function Home(): JSX.Element {
src={`/img/logos/${logo}`}
alt={name}
title={name}
style={{ maxHeight: 48, maxWidth: '100%', objectFit: 'contain' }}
style={{
maxHeight: 48,
maxWidth: '100%',
objectFit: 'contain',
}}
/>
</Card>
</a>

View File

@@ -18,6 +18,7 @@ under the License.
-->
---
title: Markdown page example
---

View File

@@ -26,16 +26,18 @@
/* You can override the default Infima variables here. */
@font-face {
font-family: 'Roboto';
src: url('../fonts/Roboto-Regular.woff2') format('woff2'),
url('../fonts/Roboto-Regular.woff') format('woff');
src:
url('../fonts/Roboto-Regular.woff2') format('woff2'),
url('../fonts/Roboto-Regular.woff') format('woff');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Roboto';
src: url('../fonts/Roboto-Bold.woff2') format('woff2'),
url('../fonts/Roboto-Bold.woff') format('woff');
src:
url('../fonts/Roboto-Bold.woff2') format('woff2'),
url('../fonts/Roboto-Bold.woff') format('woff');
font-weight: 700;
font-style: bold;
}
@@ -447,9 +449,9 @@ ul.dropdown__menu svg {
/* Limit the code editor height and make it scrollable */
/* Target multiple possible class names used by Docusaurus/react-live */
.playgroundEditor,
[class*="playgroundEditor"],
[class*='playgroundEditor'],
.live-editor,
[class*="liveEditor"] {
[class*='liveEditor'] {
max-height: 350px !important;
overflow: auto !important;
}
@@ -457,15 +459,15 @@ ul.dropdown__menu svg {
/* The actual textarea/code area inside the editor */
.playgroundEditor textarea,
.playgroundEditor pre,
[class*="playgroundEditor"] textarea,
[class*="playgroundEditor"] pre {
[class*='playgroundEditor'] textarea,
[class*='playgroundEditor'] pre {
max-height: 350px !important;
overflow: auto !important;
}
/* Also limit the preview area for consistency */
.playgroundPreview,
[class*="playgroundPreview"] {
[class*='playgroundPreview'] {
max-height: 400px;
overflow: auto;
}

View File

@@ -280,7 +280,9 @@ a > span > svg {
.footer__social-links a {
display: inline-flex;
align-items: center;
transition: opacity 0.2s, transform 0.2s;
transition:
opacity 0.2s,
transform 0.2s;
}
.footer__social-links a:hover {

View File

@@ -24,10 +24,10 @@
* so SSG can render the page without a store context.
*/
import React from "react";
import React from 'react';
import BrowserOnly from "@docusaurus/BrowserOnly";
import { useSelector } from "react-redux";
import BrowserOnly from '@docusaurus/BrowserOnly';
import { useSelector } from 'react-redux';
interface ServerVariable {
default?: string;
@@ -44,20 +44,20 @@ interface StoreState {
function colorForMethod(method: string) {
switch (method.toLowerCase()) {
case "get":
return "primary";
case "post":
return "success";
case "delete":
return "danger";
case "put":
return "info";
case "patch":
return "warning";
case "head":
return "secondary";
case "event":
return "secondary";
case 'get':
return 'primary';
case 'post':
return 'success';
case 'delete':
return 'danger';
case 'put':
return 'info';
case 'patch':
return 'warning';
case 'head':
return 'secondary';
case 'event':
return 'secondary';
default:
return undefined;
}
@@ -66,7 +66,7 @@ function colorForMethod(method: string) {
export interface Props {
method: string;
path: string;
context?: "endpoint" | "callback";
context?: 'endpoint' | 'callback';
}
// Inner component rendered only in the browser, where the Redux store exists.
@@ -74,11 +74,11 @@ function ServerUrl() {
const serverValue = useSelector((state: StoreState) => state.server.value);
if (serverValue && serverValue.variables) {
let serverUrlWithVariables = serverValue.url.replace(/\/$/, "");
Object.keys(serverValue.variables).forEach((variable) => {
let serverUrlWithVariables = serverValue.url.replace(/\/$/, '');
Object.keys(serverValue.variables).forEach(variable => {
serverUrlWithVariables = serverUrlWithVariables.replace(
`{${variable}}`,
serverValue.variables?.[variable].default ?? ""
serverValue.variables?.[variable].default ?? '',
);
});
return <>{serverUrlWithVariables}</>;
@@ -93,8 +93,8 @@ function ServerUrl() {
function MethodEndpoint({ method, path, context }: Props) {
const renderServerUrl = () => {
if (context === "callback") {
return "";
if (context === 'callback') {
return '';
}
return <BrowserOnly>{() => <ServerUrl />}</BrowserOnly>;
};
@@ -102,13 +102,13 @@ function MethodEndpoint({ method, path, context }: Props) {
return (
<>
<pre className="openapi__method-endpoint">
<span className={"badge badge--" + colorForMethod(method)}>
{method === "event" ? "Webhook" : method.toUpperCase()}
</span>{" "}
{method !== "event" && (
<span className={'badge badge--' + colorForMethod(method)}>
{method === 'event' ? 'Webhook' : method.toUpperCase()}
</span>{' '}
{method !== 'event' && (
<h2 className="openapi__method-endpoint-path">
{renderServerUrl()}
{`${path.replace(/{([a-z0-9-_]+)}/gi, ":$1")}`}
{`${path.replace(/{([a-z0-9-_]+)}/gi, ':$1')}`}
</h2>
)}
</pre>

View File

@@ -75,9 +75,13 @@ export default function DocVersionBadge() {
if (afterBase.startsWith('/')) {
const segments = afterBase.substring(1).split('/');
// Check if first segment is a version (e.g., "1.1.0", "next")
if (segments[0] && (segments[0].match(/^\d+\.\d+\.\d+$/) || segments[0] === 'next')) {
if (
segments[0] &&
(segments[0].match(/^\d+\.\d+\.\d+$/) || segments[0] === 'next')
) {
// Skip the version segment
relativePath = segments.length > 1 ? '/' + segments.slice(1).join('/') : '';
relativePath =
segments.length > 1 ? '/' + segments.slice(1).join('/') : '';
} else {
// No version in path (e.g., /docs/intro for current version with empty path)
relativePath = afterBase;

View File

@@ -75,7 +75,7 @@ function PlaygroundLivePreview(): ReactNode {
{() => (
<>
<ErrorBoundary
fallback={(params) => (
fallback={params => (
<ErrorBoundaryErrorMessageFallback {...params} />
)}
>

View File

@@ -52,12 +52,21 @@ if (isBrowser) {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { Alert } = require('@apache-superset/core/components');
console.log('[ReactLiveScope] SupersetComponents keys:', Object.keys(SupersetComponents || {}).slice(0, 10));
console.log('[ReactLiveScope] Has Button?', 'Button' in (SupersetComponents || {}));
console.log(
'[ReactLiveScope] SupersetComponents keys:',
Object.keys(SupersetComponents || {}).slice(0, 10),
);
console.log(
'[ReactLiveScope] Has Button?',
'Button' in (SupersetComponents || {}),
);
Object.assign(ReactLiveScope, SupersetComponents, { Alert });
console.log('[ReactLiveScope] Final scope keys:', Object.keys(ReactLiveScope).slice(0, 20));
console.log(
'[ReactLiveScope] Final scope keys:',
Object.keys(ReactLiveScope).slice(0, 20),
);
} catch (e) {
console.error('[ReactLiveScope] Failed to load Superset components:', e);
}

View File

@@ -21,10 +21,27 @@ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
// File extensions to track as downloads
const DOWNLOAD_EXTENSIONS = [
'pdf', 'zip', 'tar', 'gz', 'tgz', 'bz2',
'exe', 'dmg', 'pkg', 'deb', 'rpm',
'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
'csv', 'json', 'yaml', 'yml',
'pdf',
'zip',
'tar',
'gz',
'tgz',
'bz2',
'exe',
'dmg',
'pkg',
'deb',
'rpm',
'doc',
'docx',
'xls',
'xlsx',
'ppt',
'pptx',
'csv',
'json',
'yaml',
'yml',
];
// Scroll depth milestones to track
@@ -38,7 +55,9 @@ export default function Root({ children }) {
const { matomoUrl, matomoSiteId } = customFields;
if (typeof window !== 'undefined') {
const devMode = ['localhost', '127.0.0.1', '::1', '0.0.0.0'].includes(window.location.hostname);
const devMode = ['localhost', '127.0.0.1', '::1', '0.0.0.0'].includes(
window.location.hostname,
);
// Initialize the _paq array
window._paq = window._paq || [];
@@ -50,7 +69,10 @@ export default function Root({ children }) {
window._paq.push(['setSiteId', matomoSiteId]);
// Track downloads with custom extensions
window._paq.push(['setDownloadExtensions', DOWNLOAD_EXTENSIONS.join('|')]);
window._paq.push([
'setDownloadExtensions',
DOWNLOAD_EXTENSIONS.join('|'),
]);
// Now load the matomo.js script
const script = document.createElement('script');
@@ -69,7 +91,11 @@ export default function Root({ children }) {
// Helper to track site search
const trackSiteSearch = (keyword, category, resultsCount) => {
if (devMode) {
console.log('Matomo trackSiteSearch:', { keyword, category, resultsCount });
console.log('Matomo trackSiteSearch:', {
keyword,
category,
resultsCount,
});
}
window._paq.push(['trackSiteSearch', keyword, category, resultsCount]);
};
@@ -82,9 +108,8 @@ export default function Root({ children }) {
window._paq.push(['trackPageView']);
};
// Track external link clicks using domain as category (vendor-agnostic)
const handleLinkClick = (event) => {
const handleLinkClick = event => {
const link = event.target.closest('a');
if (!link) return;
@@ -106,20 +131,22 @@ export default function Root({ children }) {
// Track Algolia search queries
const setupAlgoliaTracking = () => {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
const searchInput = node.querySelector?.('.DocSearch-Input') ||
(node.classList?.contains('DocSearch-Input') ? node : null);
const searchInput =
node.querySelector?.('.DocSearch-Input') ||
(node.classList?.contains('DocSearch-Input') ? node : null);
if (searchInput) {
let debounceTimer;
searchInput.addEventListener('input', (e) => {
searchInput.addEventListener('input', e => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
const query = e.target.value.trim();
if (query.length >= 3) {
const results = document.querySelectorAll('.DocSearch-Hit');
const results =
document.querySelectorAll('.DocSearch-Hit');
trackSiteSearch(query, 'Documentation', results.length);
}
}, 1000);
@@ -135,21 +162,26 @@ export default function Root({ children }) {
};
// Track video plays
const handleVideoPlay = (event) => {
const handleVideoPlay = event => {
if (event.target.tagName === 'VIDEO') {
const videoSrc = event.target.currentSrc || event.target.src || 'unknown';
const videoSrc =
event.target.currentSrc || event.target.src || 'unknown';
trackEvent('Video', 'Play', videoSrc);
}
};
// Track CTA button clicks
const handleCTAClick = (event) => {
const button = event.target.closest('.get-started-button, .default-button-theme');
const handleCTAClick = event => {
const button = event.target.closest(
'.get-started-button, .default-button-theme',
);
if (button) {
const buttonText = button.textContent?.trim() || 'Unknown';
const clickedLink = event.target.closest?.('a');
const href =
clickedLink?.getAttribute('href') || button.getAttribute('href') || '';
clickedLink?.getAttribute('href') ||
button.getAttribute('href') ||
'';
trackEvent('CTA', 'Click', `${buttonText} - ${href}`);
}
};
@@ -158,15 +190,23 @@ export default function Root({ children }) {
let scrollMilestonesReached = new Set();
const handleScroll = () => {
const scrollTop = window.scrollY;
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
const docHeight =
document.documentElement.scrollHeight - window.innerHeight;
if (docHeight <= 0) return;
const scrollPercent = Math.round((scrollTop / docHeight) * 100);
SCROLL_MILESTONES.forEach(milestone => {
if (scrollPercent >= milestone && !scrollMilestonesReached.has(milestone)) {
if (
scrollPercent >= milestone &&
!scrollMilestonesReached.has(milestone)
) {
scrollMilestonesReached.add(milestone);
trackEvent('Scroll Depth', `${milestone}%`, window.location.pathname);
trackEvent(
'Scroll Depth',
`${milestone}%`,
window.location.pathname,
);
}
});
};
@@ -178,9 +218,13 @@ export default function Root({ children }) {
// Track 404 pages
const track404 = () => {
const is404 = document.querySelector('.theme-doc-404') ||
document.title.toLowerCase().includes('not found') ||
document.querySelector('h1')?.textContent?.toLowerCase().includes('not found');
const is404 =
document.querySelector('.theme-doc-404') ||
document.title.toLowerCase().includes('not found') ||
document
.querySelector('h1')
?.textContent?.toLowerCase()
.includes('not found');
if (is404) {
trackEvent('Error', '404', window.location.pathname);
if (devMode) {
@@ -190,19 +234,27 @@ export default function Root({ children }) {
};
// Track copy-to-clipboard events on code blocks
const handleCopy = (event) => {
const handleCopy = event => {
const codeBlock = event.target.closest('pre, code, .prism-code');
if (codeBlock) {
const codeText = window.getSelection()?.toString() || '';
const codeSnippet = codeText.substring(0, 100) + (codeText.length > 100 ? '...' : '');
trackEvent('Code', 'Copy', `${window.location.pathname}: ${codeSnippet}`);
const codeSnippet =
codeText.substring(0, 100) + (codeText.length > 100 ? '...' : '');
trackEvent(
'Code',
'Copy',
`${window.location.pathname}: ${codeSnippet}`,
);
}
};
// Track color mode preference (as event, no admin config needed)
const trackColorMode = () => {
const colorMode = document.documentElement.getAttribute('data-theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
const colorMode =
document.documentElement.getAttribute('data-theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light');
trackEvent('User Preference', 'Color Mode', colorMode);
};
@@ -289,11 +341,14 @@ export default function Root({ children }) {
window.addEventListener('scroll', handleScroll, { passive: true });
// Watch for color mode changes
const colorModeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
const colorModeObserver = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.attributeName === 'data-theme') {
trackEvent('User Preference', 'Color Mode Change',
document.documentElement.getAttribute('data-theme'));
trackEvent(
'User Preference',
'Color Mode Change',
document.documentElement.getAttribute('data-theme'),
);
}
});
});