Skip to content

Commit

Permalink
Merge branch 'main' into add_component_switch
Browse files Browse the repository at this point in the history
  • Loading branch information
JerryWu1234 authored Oct 4, 2024
2 parents 61e5eb4 + 7e87d33 commit 5d18929
Show file tree
Hide file tree
Showing 42 changed files with 886 additions and 424 deletions.
5 changes: 5 additions & 0 deletions .changeset/two-jeans-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@qwik-ui/headless': minor
---

We are removing the existing popover animations shimming and instead wil now only support native popover animations. This is considered a breaking change but will be more reliable overall.
3 changes: 2 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ node_modules
dist
coverage
.eslintrc.*
vite.config.ts
vite.config.ts
packages/kit-headless/browsers/**
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
jobs:
test:
runs-on: ubuntu-latest
name: Test NodeJS ${{ matrix.node_version }}

strategy:
matrix:
Expand Down
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
],
"editor.codeActionsOnSave": {
"source.removeUnusedImports": "explicit"
}
},
"vitest.disableWorkspaceWarning": true
}
117 changes: 117 additions & 0 deletions apps/website/auto-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import * as fs from 'fs';
import { resolve } from 'path';
import { ViteDevServer } from 'vite';
export default function autoAPI() {
return {
name: 'watch-monorepo-changes',
configureServer(server: ViteDevServer) {
const watchPath = resolve(__dirname, '../../packages/kit-headless');
server.watcher.on('change', (file: string) => {
if (file.startsWith(watchPath)) {
loopOnAllChildFiles(file);
}
});
},
};
}
// the object should have this general structure, arranged from parent to child
// componentName:[subComponent,subcomponent,...]
// subComponentName:[publicType,publicType,...]
// publicType:[{ comment,prop,type },{ comment,prop,type },...]
// THEY UPPER-MOST KEY IS ALWAYS USED AS A HEADING
export type ComponentParts = Record<string, SubComponents>;
type SubComponents = SubComponent[];
export type SubComponent = Record<string, PublicType[]>;
export type PublicType = Record<string, ParsedProps[]>;
type ParsedProps = {
comment: string;
prop: string;
type: string;
};
function parseSingleComponentFromDir(path: string, ref: SubComponents) {
const component_name = /\/([\w-]*).tsx/.exec(path);
if (component_name === null || component_name[1] === null) {
// may need better behavior
return;
}
const sourceCode = fs.readFileSync(path, 'utf-8');
const comments = extractPublicTypes(sourceCode);
const parsed: PublicType[] = [];
for (const comment of comments) {
const api = extractComments(comment.string);
const pair: PublicType = { [comment.label]: api };
parsed.push(pair);
}
const completeSubComponent: SubComponent = { [component_name[1]]: parsed };
ref.push(completeSubComponent);
return ref;
}

function extractPublicTypes(strg: string) {
const getPublicTypes = /type Public([A-Z][\w]*)*[\w\W]*?{([\w|\W]*?)}(;| &)/gm;
const cms = [];
let groups;
while ((groups = getPublicTypes.exec(strg)) !== null) {
const string = groups[2];
cms.push({ label: groups[1], string });
}
return cms;
}
function extractComments(strg: string): ParsedProps[] {
const magical_regex =
/^\s*?\/[*]{2}\n?([\w|\W|]*?)\s*[*]{1,2}[/]\n[ ]*([\w|\W]*?): ([\w|\W]*?);?$/gm;

const cms = [];
let groups;

while ((groups = magical_regex.exec(strg)) !== null) {
const trimStart = /^ *|(\* *)/g;
const comment = groups[1].replaceAll(trimStart, '');
const prop = groups[2];
const type = groups[3];
cms.push({ comment, prop, type });
}
return cms;
}
function writeToDocs(fullPath: string, componentName: string, api: ComponentParts) {
if (fullPath.includes('kit-headless')) {
const relDocPath = `../website/src/routes//docs/headless/${componentName}`;
const fullDocPath = resolve(__dirname, relDocPath);
const dirPath = fullDocPath.concat('/auto-api');

if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath);
}
const json = JSON.stringify(api, null, 2);
const hacky = `export const api=${json}`;

try {
fs.writeFileSync(dirPath.concat('/api.ts'), hacky);
console.log('auto-api: succesfully genereated new json!!! :)');
} catch (err) {
return;
}
}
}
function loopOnAllChildFiles(filePath: string) {
const childComponentRegex = /\/([\w-]*).tsx$/.exec(filePath);
if (childComponentRegex === null) {
return;
}
const parentDir = filePath.replace(childComponentRegex[0], '');
const componentRegex = /\/(\w*)$/.exec(parentDir);
if (!fs.existsSync(parentDir) || componentRegex == null) {
return;
}
const componentName = componentRegex[1];
const allParts: SubComponents = [];
const store: ComponentParts = { [componentName]: allParts };
fs.readdirSync(parentDir).forEach((fileName) => {
if (/tsx$/.test(fileName)) {
const fullPath = parentDir + '/' + fileName;
parseSingleComponentFromDir(fullPath, store[componentName]);
}
});

writeToDocs(filePath, componentName, store);
}
11 changes: 3 additions & 8 deletions apps/website/src/_state/component-statuses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,17 @@ export const statusByComponent: ComponentKitsStatuses = {
Textarea: ComponentStatus.Draft,
},
headless: {
Accordion: ComponentStatus.Beta,
Carousel: ComponentStatus.Beta,
Collapsible: ComponentStatus.Beta,
Combobox: ComponentStatus.Beta,
Checkbox: ComponentStatus.Draft,
Dropdown: ComponentStatus.Draft,
Label: ComponentStatus.Draft,
Modal: ComponentStatus.Beta,
Label: ComponentStatus.Beta,
Pagination: ComponentStatus.Draft,
Popover: ComponentStatus.Beta,
Progress: ComponentStatus.Beta,
Select: ComponentStatus.Beta,
Separator: ComponentStatus.Beta,
Tabs: ComponentStatus.Beta,
Toggle: ComponentStatus.Draft,
ToggleGroup: ComponentStatus.Draft,
Toggle: ComponentStatus.Beta,
'Toggle Group': ComponentStatus.Beta,
Tooltip: ComponentStatus.Beta,
},
};
49 changes: 49 additions & 0 deletions apps/website/src/components/animations/caveats.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { component$ } from '@builder.io/qwik';
import { Note, NoteStatus } from '../note/note'; // Adjust the import path based on your structure

export const TopLayerAnimationsCaveats = component$(() => {
return (
<Note status={NoteStatus.Warning}>
<strong>Important Caveats for Animating Discrete Properties</strong>

<ul class="mt-4 list-disc bg-gradient-to-b pl-4">
<li>
<strong>
Animating <code>display</code> and <code>overlay</code>:
</strong>
<p>
The <code>display</code> property must be included in the transitions list to
ensure the element remains visible throughout the animation. The value flips
from <code>none</code> to <code>block</code> at 0% of the animation, ensuring
visibility for the entire duration. The&nbsp;
<code>overlay</code> ensures the element stays in the top layer until the
animation completes.
</p>
</li>
<li>
<strong>
Using <code>transition-behavior: allow-discrete</code>:
</strong>
<p>
This property is essential when animating discrete properties like{' '}
<code>display</code> and <code>overlay</code>, which are not typically
animatable. It ensures smooth transitions for these discrete properties.
</p>
</li>
<li>
<strong>
Setting Starting Styles with <code>@starting-style</code>:
</strong>
<p>
CSS transitions are only triggered when a property changes on a visible
element. The&nbsp;
<code>@starting-style</code> at-rule allows you to set initial styles (e.g.,{' '}
<code>opacity</code> and
<code>transform</code>) when the element first appears, ensuring that the
animation behaves predictably.
</p>
</li>
</ul>
</Note>
);
});
22 changes: 22 additions & 0 deletions apps/website/src/components/animations/compatability.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { component$ } from '@builder.io/qwik';
import { Note, NoteStatus } from '../note/note'; // Adjust the import path based on your structure

export const BrowserAnimationsCompatability = component$(() => {
return (
<Note status={NoteStatus.Info}>
<div class="flex flex-col gap-2">
<h4>
<strong>Browser Compatability</strong>
</h4>
<p>
<a href="https://caniuse.com/?search=popover%20API">
Browser versions that do not support the popover API natively
</a>{' '}
have known issues when trying to use animations or transitions. If you need to
support legacy versions of browsers, please be sure to test this functionality
independently.
</p>
</div>
</Note>
);
});
4 changes: 2 additions & 2 deletions apps/website/src/components/api-table/api-table.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { component$ } from '@builder.io/qwik';
import { InfoPopup } from '../info-popup/info-popup';
type APITableProps = {
export type APITableProps = {
propDescriptors: {
name: string;
info: string;
info?: string;
type: string;
description: string;
}[];
Expand Down
138 changes: 138 additions & 0 deletions apps/website/src/components/api-table/auto-api.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { JSXOutput, component$, $, QRL, useTask$, useSignal } from '@builder.io/qwik';
import { APITable, type APITableProps } from './api-table';

//This is a workaround for not being able to export across packages due to nx rule:
// https://nx.dev/features/enforce-module-boundaries#enforce-module-boundaries
type ComponentParts = Record<string, SubComponents>;
type SubComponents = SubComponent[];
type SubComponent = Record<string, PublicType[]>;
type PublicType = Record<string, ParsedProps[]>;
type ParsedProps = {
comment: string;
prop: string;
type: string;
};
type AutoAPIConfig = {
topHeader?: QRL<(text: string) => JSXOutput>;
subHeader?: QRL<(text: string) => JSXOutput>;
props?: QRL<(text: string) => string>;
};

type AnatomyTableProps = {
api?: ComponentParts;
config: AutoAPIConfig;
};

type SubComponentProps = {
subComponent: SubComponent;
config: AutoAPIConfig;
};
type ParsedCommentsProps = {
parsedProps: PublicType;
config: AutoAPIConfig;
};
const currentHeader = $(() => {
//cannot send h2 from here because current TOC can only read md
return null;
});

const currentSubHeader = $((text: string) => {
let subHeader = text.replace(/(p|P)rops/, '');
const hasCapital = /[a-z][A-Z]/.exec(subHeader)?.index;
if (hasCapital != undefined) {
subHeader =
subHeader.slice(0, hasCapital + 1) + '.' + subHeader.slice(hasCapital + 1);
}
return (
<>
<h3 class="mb-6 mt-8 scroll-mt-20 text-xl font-semibold">{subHeader}</h3>
</>
);
});

const removeQuestionMarkFromProp = $((text: string) => {
return text.replace('?', '');
});
const defaultConfig: AutoAPIConfig = {
topHeader: currentHeader,
subHeader: currentSubHeader,
props: removeQuestionMarkFromProp,
};
export const AutoAPI = component$<AnatomyTableProps>(
({ api, config = defaultConfig }) => {
if (api === undefined) {
return null;
}
const key = Object.keys(api)[0];
const topHeaderSig = useSignal<string | JSXOutput>(key);
const subComponents = api[key].filter((e) => e[Object.keys(e)[0]].length > 0);
useTask$(async () => {
if (config.topHeader) {
topHeaderSig.value = await config.topHeader(key as string);
}
});
return (
<>
{topHeaderSig.value}
{subComponents.map((e, index) => (
<SubComponent key={index} subComponent={e} config={config} />
))}
</>
);
},
);

const SubComponent = component$<SubComponentProps>(({ subComponent, config }) => {
const subComponentKey = Object.keys(subComponent)[0];
const comments = subComponent[subComponentKey];
return (
<>
{comments.map((e) => (
<>
<ParsedComments parsedProps={e} config={config} />
</>
))}
</>
);
});

const ParsedComments = component$<ParsedCommentsProps>(({ parsedProps, config }) => {
const key = Object.keys(parsedProps)[0];
const subHeaderSig = useSignal<string | JSXOutput>(key);
useTask$(async () => {
if (config.subHeader) {
subHeaderSig.value = await config.subHeader(key as string);
}
});
const appliedPropsSig = useSignal<null | APITableProps>(null);
useTask$(async () => {
const translation: APITableProps = {
propDescriptors: parsedProps[key].map((e) => {
const isObject = e.type.includes('{');
const isUnion = e.type.includes('|');
const isPopup = isObject || isUnion;

return {
name: e.prop,
type: isObject ? 'object' : isUnion ? 'union' : e.type,
description: e.comment,
info: (isPopup && e.type) || undefined,
};
}),
};
if (config.props) {
for (const props of translation.propDescriptors) {
props.name = await config.props(props.name);
}
}
appliedPropsSig.value = translation;
});
return (
<>
{subHeaderSig.value}
{appliedPropsSig.value?.propDescriptors && (
<APITable propDescriptors={appliedPropsSig.value?.propDescriptors} />
)}
</>
);
});
Loading

0 comments on commit 5d18929

Please sign in to comment.