UI Consolidation Cont.
This commit is contained in:
@@ -47,3 +47,6 @@ export default function PageHead(props: PageHeadProps) {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Named export for consistency
|
||||
export { PageHead };
|
||||
|
||||
@@ -250,6 +250,9 @@ declare module "@tiptap/core" {
|
||||
iframe: {
|
||||
setIframe: (options: { src: string }) => ReturnType;
|
||||
};
|
||||
video: {
|
||||
setVideo: (options: { src: string }) => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,24 +301,76 @@ const IframeEmbed = Node.create<IframeOptions>({
|
||||
return {
|
||||
setIframe:
|
||||
(options: { src: string }) =>
|
||||
({ tr, dispatch, editor }) => {
|
||||
({ tr, dispatch }) => {
|
||||
const { selection } = tr;
|
||||
const node = this.type.create(options);
|
||||
|
||||
// Check if the src is a direct video file
|
||||
const src = options.src || "";
|
||||
const isVideoFile = /\.(mp4|mov|webm|ogg)(\?.*)?$/i.test(src);
|
||||
|
||||
if (isVideoFile) {
|
||||
// Insert a proper video tag instead of iframe
|
||||
if (dispatch) {
|
||||
const videoHTML = `<video src="${src}" controls playsinline style="max-width: 100%; height: auto;"></video>`;
|
||||
editor.commands.insertContent(videoHTML);
|
||||
}
|
||||
return true;
|
||||
if (dispatch) {
|
||||
tr.replaceRangeWith(selection.from, selection.to, node);
|
||||
}
|
||||
|
||||
// For non-video URLs, create iframe as normal
|
||||
const node = this.type.create(options);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
interface VideoOptions {
|
||||
HTMLAttributes: {
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
const Video = Node.create<VideoOptions>({
|
||||
name: "video",
|
||||
group: "block",
|
||||
atom: true,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {
|
||||
class: "video-wrapper"
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: null
|
||||
},
|
||||
controls: {
|
||||
default: true
|
||||
},
|
||||
playsinline: {
|
||||
default: true
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "video[src]"
|
||||
}
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["video", HTMLAttributes];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setVideo:
|
||||
(options: { src: string }) =>
|
||||
({ tr, dispatch }) => {
|
||||
const { selection } = tr;
|
||||
const node = this.type.create({
|
||||
src: options.src,
|
||||
controls: true,
|
||||
playsinline: true
|
||||
});
|
||||
|
||||
if (dispatch) {
|
||||
tr.replaceRangeWith(selection.from, selection.to, node);
|
||||
@@ -1352,6 +1407,7 @@ export default function TextEditor(props: TextEditorProps) {
|
||||
}),
|
||||
Image,
|
||||
IframeEmbed,
|
||||
Video,
|
||||
TaskList,
|
||||
TaskItem.configure({
|
||||
nested: true,
|
||||
@@ -2454,12 +2510,22 @@ export default function TextEditor(props: TextEditorProps) {
|
||||
const instance = editor();
|
||||
if (!instance) return;
|
||||
|
||||
const url = window.prompt("URL");
|
||||
const url = window.prompt("Embed URL (YouTube, etc.)");
|
||||
if (url) {
|
||||
instance.commands.setIframe({ src: url });
|
||||
}
|
||||
};
|
||||
|
||||
const addVideo = () => {
|
||||
const instance = editor();
|
||||
if (!instance) return;
|
||||
|
||||
const url = window.prompt("Video URL (direct link to .mp4, .webm, etc.)");
|
||||
if (url) {
|
||||
instance.commands.setVideo({ src: url });
|
||||
}
|
||||
};
|
||||
|
||||
const addImage = () => {
|
||||
const instance = editor();
|
||||
if (!instance) return;
|
||||
@@ -3790,13 +3856,21 @@ export default function TextEditor(props: TextEditorProps) {
|
||||
>
|
||||
🖼 Image
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addVideo}
|
||||
class="touch-manipulation rounded px-2 py-1 text-xs select-none"
|
||||
title="Add Video"
|
||||
>
|
||||
🎬 Video
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addIframe}
|
||||
class="touch-manipulation rounded px-2 py-1 text-xs select-none"
|
||||
title="Add Iframe"
|
||||
title="Add Iframe Embed"
|
||||
>
|
||||
📺 Iframe
|
||||
📺 Embed
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -23,7 +23,7 @@ export default function Button(props: ButtonProps) {
|
||||
const size = () => local.size || "md";
|
||||
|
||||
const baseClasses =
|
||||
"flex justify-center items-center rounded transition-all duration-300 ease-out";
|
||||
"flex justify-center cursor-pointer items-center rounded transition-all duration-300 ease-out";
|
||||
|
||||
const variantClasses = () => {
|
||||
const isDisabledOrLoading = local.disabled || local.loading;
|
||||
@@ -77,3 +77,6 @@ export default function Button(props: ButtonProps) {
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// Named export for consistency
|
||||
export { Button };
|
||||
|
||||
@@ -24,7 +24,7 @@ export default function PasswordInput(props: PasswordInputProps) {
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div class={local.containerClass || "input-group relative mx-4 mb-2"}>
|
||||
<Input
|
||||
{...inputProps}
|
||||
@@ -36,7 +36,7 @@ export default function PasswordInput(props: PasswordInputProps) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword())}
|
||||
class="text-subtext0 absolute top-2 right-0 transition-all hover:brightness-125"
|
||||
class="text-subtext0 absolute right-0 bottom-2 transition-all hover:brightness-125"
|
||||
aria-label={showPassword() ? "Hide password" : "Show password"}
|
||||
>
|
||||
<Show
|
||||
@@ -56,10 +56,8 @@ export default function PasswordInput(props: PasswordInputProps) {
|
||||
</div>
|
||||
|
||||
{local.showStrength && local.passwordValue !== undefined && (
|
||||
<div class="px-4 pt-1">
|
||||
<PasswordStrengthMeter password={local.passwordValue} />
|
||||
</div>
|
||||
<PasswordStrengthMeter password={local.passwordValue} />
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user