Add initial project files
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
*.md
|
||||
node_modules
|
||||
dist
|
||||
public
|
||||
build
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"endOfLine": "lf",
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"semi": true,
|
||||
"bracketSameLine": false,
|
||||
"jsxSingleQuote": false,
|
||||
"arrowParens": "always"
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
# @crux-kit/editor-tiptap
|
||||
|
||||
A free, headless rich-text editor for Crux apps, built on [TipTap](https://tiptap.dev)
|
||||
(MIT-licensed). Emits and consumes plain **HTML strings**, so it drops into the same
|
||||
`value` / `onChange` seam the previous TinyMCE editor used.
|
||||
|
||||
This package contains **all TipTap-specific editor code**. Other editor libraries live in
|
||||
their own sibling packages (e.g. `@crux-kit/editor-quill`, `@crux-kit/editor-lexical`).
|
||||
|
||||
The editor is **preset-driven**: one library, multiple feature levels. Pick a preset per
|
||||
app / per field instead of shipping a different editor library in each app.
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { RichTextEditor } from '@crux-kit/editor-tiptap';
|
||||
|
||||
<RichTextEditor value={html} onChange={setHtml} preset="light" placeholder="Write…" />;
|
||||
```
|
||||
|
||||
### Presets
|
||||
|
||||
| Preset | Features |
|
||||
| ------- | ------------------------------------------------------------------------------------------- |
|
||||
| `light` | Bold, italic, underline, strike, headings, lists, blockquote, text align, links, undo/redo. |
|
||||
|
||||
Extend by adding new presets in `src/presets.ts` — the toolbar renders from the preset, so
|
||||
new apps (e.g. web studio / page manager) reuse the same component with a different preset.
|
||||
|
||||
## Styling
|
||||
|
||||
The toolbar uses the shared design-system Tailwind tokens (`bg-accent`, `border-input`,
|
||||
`text-muted-foreground`, …), so it themes automatically with the host app.
|
||||
|
||||
Wire it up once, entirely in the app's main Tailwind CSS entry:
|
||||
|
||||
```css
|
||||
/* generate the toolbar's utility classes */
|
||||
@source '../../../../../packages/design-system/editor-tiptap/src';
|
||||
/* editor content styles */
|
||||
@import '@crux-kit/editor-tiptap/styles.css';
|
||||
```
|
||||
|
||||
(The content styles can also be pulled in from JS with
|
||||
`import '@crux-kit/editor-tiptap/styles.css';` — but keeping both lines together in the CSS
|
||||
entry is cleaner.)
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@crux-kit/editor-tiptap",
|
||||
"version": "1.3.1",
|
||||
"type": "module",
|
||||
"types": "src/index.ts",
|
||||
"private": true,
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./styles.css": "./src/styles.css"
|
||||
},
|
||||
"style": "src/styles.css",
|
||||
"scripts": {
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@crux-kit/utils": "*",
|
||||
"@tiptap/core": "^3.29.2",
|
||||
"@tiptap/extension-text-align": "^3.29.2",
|
||||
"@tiptap/extensions": "^3.29.2",
|
||||
"@tiptap/pm": "^3.29.2",
|
||||
"@tiptap/react": "^3.29.2",
|
||||
"@tiptap/starter-kit": "^3.29.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"lucide-react": "*",
|
||||
"react": ">=19",
|
||||
"react-dom": ">=19"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { type Editor, EditorContent, useEditor } from '@tiptap/react';
|
||||
import React from 'react';
|
||||
import { cn } from '@crux-kit/utils';
|
||||
import { Toolbar } from './components/Toolbar';
|
||||
import { buildExtensions, type EditorPreset } from './presets';
|
||||
|
||||
export type RichTextEditorProps = {
|
||||
/** Current value as an HTML string. */
|
||||
value: string;
|
||||
/** Called with the new HTML string on every change. */
|
||||
onChange: (value: string) => void;
|
||||
/** Feature level. Defaults to `light`. */
|
||||
preset?: EditorPreset;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
/** Access the underlying TipTap editor instance once it is ready. */
|
||||
onReady?: (editor: Editor) => void;
|
||||
};
|
||||
|
||||
export const RichTextEditor: React.FC<RichTextEditorProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
preset = 'light',
|
||||
disabled = false,
|
||||
placeholder,
|
||||
className,
|
||||
onReady,
|
||||
}) => {
|
||||
const onChangeRef = React.useRef(onChange);
|
||||
onChangeRef.current = onChange;
|
||||
const onReadyRef = React.useRef(onReady);
|
||||
onReadyRef.current = onReady;
|
||||
|
||||
const editor = useEditor({
|
||||
immediatelyRender: false,
|
||||
editable: !disabled,
|
||||
extensions: buildExtensions(preset, placeholder),
|
||||
content: value,
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: 'crux-editor__content',
|
||||
role: 'textbox',
|
||||
'aria-multiline': 'true',
|
||||
},
|
||||
},
|
||||
onCreate: ({ editor }) => onReadyRef.current?.(editor),
|
||||
onUpdate: ({ editor }) => onChangeRef.current(editor.getHTML()),
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!editor) return;
|
||||
const next = value ?? '';
|
||||
if (next !== editor.getHTML()) {
|
||||
editor.commands.setContent(next, { emitUpdate: false });
|
||||
}
|
||||
}, [editor, value]);
|
||||
|
||||
React.useEffect(() => {
|
||||
editor?.setEditable(!disabled);
|
||||
}, [editor, disabled]);
|
||||
|
||||
return (
|
||||
<div className={cn('crux-editor overflow-hidden rounded-md', className)}>
|
||||
<Toolbar editor={editor} preset={preset} disabled={disabled} />
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { cn } from '@crux-kit/utils';
|
||||
import { type BlockType } from '../presets';
|
||||
|
||||
const OPTIONS: Array<{ value: BlockType; label: string }> = [
|
||||
{ value: 'paragraph', label: 'Paragraph' },
|
||||
{ value: 'h1', label: 'Heading 1' },
|
||||
{ value: 'h2', label: 'Heading 2' },
|
||||
{ value: 'h3', label: 'Heading 3' },
|
||||
];
|
||||
|
||||
export const BlockTypeSelect: React.FC<{
|
||||
value: BlockType;
|
||||
disabled?: boolean;
|
||||
onChange: (value: BlockType) => void;
|
||||
}> = ({ value, disabled = false, onChange }) => {
|
||||
return (
|
||||
<select
|
||||
aria-label="Text style"
|
||||
title="Text style"
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onChange={(e) => onChange(e.target.value as BlockType)}
|
||||
className={cn(
|
||||
'border-input bg-background text-foreground h-8 rounded-md border px-2 text-sm',
|
||||
'hover:bg-muted focus-visible:ring-ring focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-1',
|
||||
'disabled:pointer-events-none disabled:opacity-40',
|
||||
)}
|
||||
>
|
||||
{OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
import { Check, Link2, Trash2 } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { cn } from '@crux-kit/utils';
|
||||
import { ToolbarButton } from './ToolbarButton';
|
||||
|
||||
export const LinkEditor: React.FC<{
|
||||
isActive: boolean;
|
||||
disabled?: boolean;
|
||||
getHref: () => string;
|
||||
onApply: (href: string) => void;
|
||||
onRemove: () => void;
|
||||
}> = ({ isActive, disabled = false, getHref, onApply, onRemove }) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [href, setHref] = React.useState('');
|
||||
const wrapperRef = React.useRef<HTMLSpanElement>(null);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const close = React.useCallback(() => setOpen(false), []);
|
||||
|
||||
const toggle = React.useCallback(() => {
|
||||
setOpen((prev) => {
|
||||
const next = !prev;
|
||||
if (next) setHref(getHref());
|
||||
return next;
|
||||
});
|
||||
}, [getHref]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open) return;
|
||||
const id = window.setTimeout(() => inputRef.current?.focus(), 0);
|
||||
|
||||
const onPointerDown = (e: MouseEvent) => {
|
||||
if (!wrapperRef.current?.contains(e.target as Node)) close();
|
||||
};
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') close();
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', onPointerDown);
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
return () => {
|
||||
window.clearTimeout(id);
|
||||
document.removeEventListener('mousedown', onPointerDown);
|
||||
document.removeEventListener('keydown', onKeyDown);
|
||||
};
|
||||
}, [open, close]);
|
||||
|
||||
const apply = () => {
|
||||
const trimmed = href.trim();
|
||||
if (trimmed) onApply(trimmed);
|
||||
else onRemove();
|
||||
close();
|
||||
};
|
||||
|
||||
const remove = () => {
|
||||
onRemove();
|
||||
close();
|
||||
};
|
||||
|
||||
return (
|
||||
<span ref={wrapperRef} className="relative inline-flex">
|
||||
<ToolbarButton title="Insert link" active={isActive} disabled={disabled} onClick={toggle}>
|
||||
<Link2 />
|
||||
</ToolbarButton>
|
||||
|
||||
{open && (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-label="Edit link"
|
||||
className={cn(
|
||||
'border-border bg-popover text-popover-foreground absolute top-full left-0 z-50 mt-1 flex w-72 items-center gap-1 rounded-md border p-1.5 shadow-md',
|
||||
)}
|
||||
>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="url"
|
||||
value={href}
|
||||
placeholder="https://example.com"
|
||||
onChange={(e) => setHref(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
apply();
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'border-input bg-background text-foreground h-8 flex-1 rounded-md border px-2 text-sm',
|
||||
'focus-visible:ring-ring focus-visible:outline-hidden focus-visible:ring-2',
|
||||
)}
|
||||
/>
|
||||
<ToolbarButton title="Apply link" onClick={apply}>
|
||||
<Check />
|
||||
</ToolbarButton>
|
||||
{isActive && (
|
||||
<ToolbarButton title="Remove link" onClick={remove}>
|
||||
<Trash2 />
|
||||
</ToolbarButton>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,285 @@
|
||||
import {
|
||||
AlignCenter,
|
||||
AlignJustify,
|
||||
AlignLeft,
|
||||
AlignRight,
|
||||
Bold,
|
||||
Italic,
|
||||
List,
|
||||
ListOrdered,
|
||||
Quote,
|
||||
RemoveFormatting,
|
||||
Redo2,
|
||||
Strikethrough,
|
||||
Underline,
|
||||
Undo2,
|
||||
Unlink,
|
||||
} from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { type Editor, useEditorState } from '@tiptap/react';
|
||||
import { type BlockType, type ToolbarItem, toolbarLayouts, type EditorPreset } from '../presets';
|
||||
import { BlockTypeSelect } from './BlockTypeSelect';
|
||||
import { LinkEditor } from './LinkEditor';
|
||||
import { ToolbarButton, ToolbarSeparator } from './ToolbarButton';
|
||||
|
||||
function getBlockType(editor: Editor): BlockType {
|
||||
if (editor.isActive('heading', { level: 1 })) return 'h1';
|
||||
if (editor.isActive('heading', { level: 2 })) return 'h2';
|
||||
if (editor.isActive('heading', { level: 3 })) return 'h3';
|
||||
return 'paragraph';
|
||||
}
|
||||
|
||||
export const Toolbar: React.FC<{
|
||||
editor: Editor | null;
|
||||
preset: EditorPreset;
|
||||
disabled?: boolean;
|
||||
}> = ({ editor, preset, disabled = false }) => {
|
||||
const state = useEditorState({
|
||||
editor,
|
||||
selector: ({ editor: e }) => {
|
||||
if (!e) return null;
|
||||
return {
|
||||
canUndo: e.can().undo(),
|
||||
canRedo: e.can().redo(),
|
||||
blockType: getBlockType(e),
|
||||
isBold: e.isActive('bold'),
|
||||
isItalic: e.isActive('italic'),
|
||||
isUnderline: e.isActive('underline'),
|
||||
isStrike: e.isActive('strike'),
|
||||
isBulletList: e.isActive('bulletList'),
|
||||
isOrderedList: e.isActive('orderedList'),
|
||||
isBlockquote: e.isActive('blockquote'),
|
||||
alignLeft: e.isActive({ textAlign: 'left' }),
|
||||
alignCenter: e.isActive({ textAlign: 'center' }),
|
||||
alignRight: e.isActive({ textAlign: 'right' }),
|
||||
alignJustify: e.isActive({ textAlign: 'justify' }),
|
||||
isLink: e.isActive('link'),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
if (!editor || !state) return null;
|
||||
|
||||
const setBlockType = (type: BlockType) => {
|
||||
const chain = editor.chain().focus();
|
||||
if (type === 'paragraph') chain.setParagraph().run();
|
||||
else chain.setHeading({ level: Number(type.slice(1)) as 1 | 2 | 3 }).run();
|
||||
};
|
||||
|
||||
const renderItem = (item: ToolbarItem, index: number): React.ReactNode => {
|
||||
switch (item) {
|
||||
case '|':
|
||||
return <ToolbarSeparator key={`sep-${index}`} />;
|
||||
case 'undo':
|
||||
return (
|
||||
<ToolbarButton
|
||||
key={item}
|
||||
title="Undo"
|
||||
disabled={disabled || !state.canUndo}
|
||||
onClick={() => editor.chain().focus().undo().run()}
|
||||
>
|
||||
<Undo2 />
|
||||
</ToolbarButton>
|
||||
);
|
||||
case 'redo':
|
||||
return (
|
||||
<ToolbarButton
|
||||
key={item}
|
||||
title="Redo"
|
||||
disabled={disabled || !state.canRedo}
|
||||
onClick={() => editor.chain().focus().redo().run()}
|
||||
>
|
||||
<Redo2 />
|
||||
</ToolbarButton>
|
||||
);
|
||||
case 'blockType':
|
||||
return (
|
||||
<BlockTypeSelect
|
||||
key={item}
|
||||
value={state.blockType}
|
||||
disabled={disabled}
|
||||
onChange={setBlockType}
|
||||
/>
|
||||
);
|
||||
case 'bold':
|
||||
return (
|
||||
<ToolbarButton
|
||||
key={item}
|
||||
title="Bold"
|
||||
active={state.isBold}
|
||||
disabled={disabled}
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
>
|
||||
<Bold />
|
||||
</ToolbarButton>
|
||||
);
|
||||
case 'italic':
|
||||
return (
|
||||
<ToolbarButton
|
||||
key={item}
|
||||
title="Italic"
|
||||
active={state.isItalic}
|
||||
disabled={disabled}
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
>
|
||||
<Italic />
|
||||
</ToolbarButton>
|
||||
);
|
||||
case 'underline':
|
||||
return (
|
||||
<ToolbarButton
|
||||
key={item}
|
||||
title="Underline"
|
||||
active={state.isUnderline}
|
||||
disabled={disabled}
|
||||
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
||||
>
|
||||
<Underline />
|
||||
</ToolbarButton>
|
||||
);
|
||||
case 'strike':
|
||||
return (
|
||||
<ToolbarButton
|
||||
key={item}
|
||||
title="Strikethrough"
|
||||
active={state.isStrike}
|
||||
disabled={disabled}
|
||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||
>
|
||||
<Strikethrough />
|
||||
</ToolbarButton>
|
||||
);
|
||||
case 'bulletList':
|
||||
return (
|
||||
<ToolbarButton
|
||||
key={item}
|
||||
title="Bullet list"
|
||||
active={state.isBulletList}
|
||||
disabled={disabled}
|
||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||
>
|
||||
<List />
|
||||
</ToolbarButton>
|
||||
);
|
||||
case 'orderedList':
|
||||
return (
|
||||
<ToolbarButton
|
||||
key={item}
|
||||
title="Numbered list"
|
||||
active={state.isOrderedList}
|
||||
disabled={disabled}
|
||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||
>
|
||||
<ListOrdered />
|
||||
</ToolbarButton>
|
||||
);
|
||||
case 'blockquote':
|
||||
return (
|
||||
<ToolbarButton
|
||||
key={item}
|
||||
title="Quote"
|
||||
active={state.isBlockquote}
|
||||
disabled={disabled}
|
||||
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
||||
>
|
||||
<Quote />
|
||||
</ToolbarButton>
|
||||
);
|
||||
case 'alignLeft':
|
||||
return (
|
||||
<ToolbarButton
|
||||
key={item}
|
||||
title="Align left"
|
||||
active={state.alignLeft}
|
||||
disabled={disabled}
|
||||
onClick={() => editor.chain().focus().setTextAlign('left').run()}
|
||||
>
|
||||
<AlignLeft />
|
||||
</ToolbarButton>
|
||||
);
|
||||
case 'alignCenter':
|
||||
return (
|
||||
<ToolbarButton
|
||||
key={item}
|
||||
title="Align center"
|
||||
active={state.alignCenter}
|
||||
disabled={disabled}
|
||||
onClick={() => editor.chain().focus().setTextAlign('center').run()}
|
||||
>
|
||||
<AlignCenter />
|
||||
</ToolbarButton>
|
||||
);
|
||||
case 'alignRight':
|
||||
return (
|
||||
<ToolbarButton
|
||||
key={item}
|
||||
title="Align right"
|
||||
active={state.alignRight}
|
||||
disabled={disabled}
|
||||
onClick={() => editor.chain().focus().setTextAlign('right').run()}
|
||||
>
|
||||
<AlignRight />
|
||||
</ToolbarButton>
|
||||
);
|
||||
case 'alignJustify':
|
||||
return (
|
||||
<ToolbarButton
|
||||
key={item}
|
||||
title="Justify"
|
||||
active={state.alignJustify}
|
||||
disabled={disabled}
|
||||
onClick={() => editor.chain().focus().setTextAlign('justify').run()}
|
||||
>
|
||||
<AlignJustify />
|
||||
</ToolbarButton>
|
||||
);
|
||||
case 'link':
|
||||
return (
|
||||
<LinkEditor
|
||||
key={item}
|
||||
isActive={state.isLink}
|
||||
disabled={disabled}
|
||||
getHref={() => (editor.getAttributes('link')['href'] as string) ?? ''}
|
||||
onApply={(href) =>
|
||||
editor.chain().focus().extendMarkRange('link').setLink({ href }).run()
|
||||
}
|
||||
onRemove={() => editor.chain().focus().extendMarkRange('link').unsetLink().run()}
|
||||
/>
|
||||
);
|
||||
case 'unlink':
|
||||
return (
|
||||
<ToolbarButton
|
||||
key={item}
|
||||
title="Remove link"
|
||||
disabled={disabled || !state.isLink}
|
||||
onClick={() => editor.chain().focus().extendMarkRange('link').unsetLink().run()}
|
||||
>
|
||||
<Unlink />
|
||||
</ToolbarButton>
|
||||
);
|
||||
case 'clear':
|
||||
return (
|
||||
<ToolbarButton
|
||||
key={item}
|
||||
title="Clear formatting"
|
||||
disabled={disabled}
|
||||
onClick={() => editor.chain().focus().clearNodes().unsetAllMarks().run()}
|
||||
>
|
||||
<RemoveFormatting />
|
||||
</ToolbarButton>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
role="toolbar"
|
||||
aria-label="Formatting"
|
||||
className="border-input flex flex-wrap items-center gap-0.5 rounded-t-md border border-b-0 p-1"
|
||||
>
|
||||
{toolbarLayouts[preset].map((item, index) => renderItem(item, index))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { cn } from '@crux-kit/utils';
|
||||
|
||||
export type ToolbarButtonProps = {
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
title: string;
|
||||
onClick: () => void;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* A single toolbar control. Styled with the shared design-system Tailwind
|
||||
* tokens so it matches the host app in light and dark mode. Kept dependency-free
|
||||
* (plain <button>) so the package stays portable across apps.
|
||||
*/
|
||||
export const ToolbarButton = React.forwardRef<HTMLButtonElement, ToolbarButtonProps>(
|
||||
({ active = false, disabled = false, title, onClick, children }, ref) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
type="button"
|
||||
title={title}
|
||||
aria-label={title}
|
||||
aria-pressed={active}
|
||||
data-active={active}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
// Prevent the editor from losing its selection when the control is pressed.
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
className={cn(
|
||||
'text-muted-foreground inline-flex h-8 min-w-8 shrink-0 items-center justify-center rounded-md px-2 text-sm transition-colors',
|
||||
'hover:bg-muted hover:text-foreground focus-visible:ring-ring focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-1',
|
||||
'disabled:pointer-events-none disabled:opacity-40',
|
||||
'data-[active=true]:bg-accent data-[active=true]:text-accent-foreground',
|
||||
'[&_svg]:size-4 [&_svg]:shrink-0',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ToolbarButton.displayName = 'ToolbarButton';
|
||||
|
||||
export const ToolbarSeparator: React.FC = () => (
|
||||
<span aria-hidden className="bg-border mx-1 h-5 w-px shrink-0" />
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
export { RichTextEditor, type RichTextEditorProps } from './RichTextEditor';
|
||||
export {
|
||||
type EditorPreset,
|
||||
type ToolbarItem,
|
||||
type BlockType,
|
||||
toolbarLayouts,
|
||||
buildExtensions,
|
||||
} from './presets';
|
||||
export { type Editor } from '@tiptap/react';
|
||||
@@ -0,0 +1,84 @@
|
||||
import { type AnyExtension } from '@tiptap/core';
|
||||
import { TextAlign } from '@tiptap/extension-text-align';
|
||||
import { Placeholder } from '@tiptap/extensions';
|
||||
import { StarterKit } from '@tiptap/starter-kit';
|
||||
|
||||
export type EditorPreset = 'light';
|
||||
|
||||
export type ToolbarItem =
|
||||
| '|'
|
||||
| 'undo'
|
||||
| 'redo'
|
||||
| 'blockType'
|
||||
| 'bold'
|
||||
| 'italic'
|
||||
| 'underline'
|
||||
| 'strike'
|
||||
| 'bulletList'
|
||||
| 'orderedList'
|
||||
| 'blockquote'
|
||||
| 'alignLeft'
|
||||
| 'alignCenter'
|
||||
| 'alignRight'
|
||||
| 'alignJustify'
|
||||
| 'link'
|
||||
| 'unlink'
|
||||
| 'clear';
|
||||
|
||||
export type BlockType = 'paragraph' | 'h1' | 'h2' | 'h3';
|
||||
|
||||
export const HEADING_LEVELS = [1, 2, 3] as const;
|
||||
|
||||
export function buildExtensions(preset: EditorPreset, placeholder?: string): AnyExtension[] {
|
||||
switch (preset) {
|
||||
case 'light':
|
||||
default:
|
||||
return [
|
||||
StarterKit.configure({
|
||||
heading: { levels: [...HEADING_LEVELS] },
|
||||
link: {
|
||||
openOnClick: false,
|
||||
autolink: true,
|
||||
defaultProtocol: 'https',
|
||||
HTMLAttributes: { rel: 'noopener noreferrer nofollow', target: '_blank' },
|
||||
},
|
||||
codeBlock: false,
|
||||
code: false,
|
||||
horizontalRule: false,
|
||||
}),
|
||||
TextAlign.configure({ types: ['heading', 'paragraph'] }),
|
||||
Placeholder.configure({
|
||||
placeholder: placeholder ?? '',
|
||||
showOnlyWhenEditable: true,
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export const toolbarLayouts: Record<EditorPreset, ToolbarItem[]> = {
|
||||
light: [
|
||||
'undo',
|
||||
'redo',
|
||||
'|',
|
||||
'blockType',
|
||||
'|',
|
||||
'bold',
|
||||
'italic',
|
||||
'underline',
|
||||
'strike',
|
||||
'|',
|
||||
'bulletList',
|
||||
'orderedList',
|
||||
'blockquote',
|
||||
'|',
|
||||
'alignLeft',
|
||||
'alignCenter',
|
||||
'alignRight',
|
||||
'alignJustify',
|
||||
'|',
|
||||
'link',
|
||||
'unlink',
|
||||
'|',
|
||||
'clear',
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Content styles for @crux-kit/editor-tiptap.
|
||||
* Plain CSS on purpose: this file is imported directly by consumers and must not
|
||||
* depend on the host app's Tailwind pipeline. Colors reference the shared
|
||||
* design-system CSS variables (--foreground, --border, …) so the editor themes
|
||||
* automatically in light/dark mode.
|
||||
*/
|
||||
|
||||
.crux-editor__content {
|
||||
min-height: 12rem;
|
||||
padding: 0.75rem 0.875rem;
|
||||
outline: none;
|
||||
color: var(--foreground);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
border-bottom: 1px solid var(--color-input);
|
||||
border-left: 1px solid var(--color-input);
|
||||
border-right: 1px solid var(--color-input);
|
||||
border-bottom-right-radius: var(--radius-md);
|
||||
border-bottom-left-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.crux-editor__content :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.crux-editor__content :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.crux-editor__content p {
|
||||
margin: 0 0 0.75em;
|
||||
}
|
||||
|
||||
.crux-editor__content h1,
|
||||
.crux-editor__content h2,
|
||||
.crux-editor__content h3 {
|
||||
margin: 1.25em 0 0.5em;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.crux-editor__content h1 {
|
||||
font-size: 1.75em;
|
||||
}
|
||||
|
||||
.crux-editor__content h2 {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.crux-editor__content h3 {
|
||||
font-size: 1.15em;
|
||||
}
|
||||
|
||||
.crux-editor__content ul,
|
||||
.crux-editor__content ol {
|
||||
margin: 0 0 0.75em;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.crux-editor__content ul {
|
||||
list-style: disc;
|
||||
}
|
||||
|
||||
.crux-editor__content ol {
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
.crux-editor__content li {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
.crux-editor__content li p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.crux-editor__content blockquote {
|
||||
margin: 0 0 0.75em;
|
||||
padding-left: 1rem;
|
||||
border-left: 3px solid var(--border);
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.crux-editor__content a {
|
||||
color: var(--primary);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.crux-editor__content strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Placeholder (from @tiptap/extensions Placeholder) */
|
||||
.crux-editor__content p.is-editor-empty:first-child::before {
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
/* ProseMirror selection niceties */
|
||||
.crux-editor__content .ProseMirror-selectednode {
|
||||
outline: 2px solid var(--ring);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2023", "DOM"],
|
||||
"jsx": "react-jsx",
|
||||
"skipLibCheck": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"composite": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
*.md
|
||||
node_modules
|
||||
dist
|
||||
public
|
||||
build
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"endOfLine": "lf",
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"semi": true,
|
||||
"bracketSameLine": false,
|
||||
"jsxSingleQuote": false,
|
||||
"arrowParens": "always"
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
# [←](../../../docs/README.md) @crux-kit/keenicon
|
||||
|
||||
Документація для Keenicon реакт компонентів/іконок.
|
||||
|
||||
Компонент KeenIcon — це універсальний інструмент для відображення іконок системи KeenIcon. Він підтримує різні стилі (filled, outline, solid, duotone)
|
||||
|
||||
Приклади іконок знаходяться у:
|
||||
`/assets/duotone/demo.html`
|
||||
`/assets/filled/demo.html`
|
||||
`/assets/outline/demo.html`
|
||||
`/assets/solid/demo.html`
|
||||
|
||||
### 1. Підключення стилів
|
||||
|
||||
Перед використанням компонента потрібно імпортувати основні стилі іконок у головний файл (наприклад, app.tsx або main.tsx)
|
||||
|
||||
```jsx
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import '@crux-kit/keenicon/assets/styles.css'
|
||||
import "./css/styles.css";
|
||||
import { App } from "./app";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
||||
```
|
||||
|
||||
### 2. Використання компонента
|
||||
|
||||
Щоб використати іконку `ki-abstract-1` потрібно передали в проп `icon` значкення `abstract-1`.
|
||||
Щоб змінити колір іконки потрібно передати у `className` класс з проп `color: red`.
|
||||
|
||||
```css
|
||||
/*
|
||||
Tailwind
|
||||
className="text-destructive"
|
||||
*/
|
||||
.text-destructive {
|
||||
color: red;
|
||||
}
|
||||
```
|
||||
|
||||
```jsx
|
||||
import { KeenIcon } from './components/KeenIcon';
|
||||
|
||||
const MyComponent = () => {
|
||||
return (
|
||||
<div>
|
||||
<KeenIcon icon="abstract-1" />
|
||||
|
||||
<KeenIcon icon="abstract-1" variant="outline" size="2x" className="text-primary" />
|
||||
|
||||
<KeenIcon icon="abstract-1" variant="duotone" className="text-destructive" size="5x" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Налаштування
|
||||
|
||||
| Prop | Тип | За замовчуванням | Опис |
|
||||
| --------- | --------------- | ---------------- | --------------------------------------------------------- |
|
||||
| icon | string | Обов'язково | Назва іконки (наприклад, magnifier, abstract-1) |
|
||||
| variant | KeeniconVariant | 'filled' | Стиль іконки: duotone, filled, solid, outline |
|
||||
| size | KeeniconSize | 'md' | Розмір іконки від 12px до 64px |
|
||||
| className | string | '' | Додаткові CSS-класи (наприклад, для кольору чи відступів) |
|
||||
|
||||
### 4. Розмірна сітка
|
||||
|
||||
```css
|
||||
.ki-xs {
|
||||
font-size: 12px;
|
||||
}
|
||||
.ki-sm {
|
||||
font-size: 14px;
|
||||
}
|
||||
.ki-md {
|
||||
font-size: 16px;
|
||||
}
|
||||
.ki-lg {
|
||||
font-size: 18px;
|
||||
}
|
||||
.ki-xl {
|
||||
font-size: 20px;
|
||||
}
|
||||
.ki-2x {
|
||||
font-size: 24px;
|
||||
}
|
||||
.ki-3x {
|
||||
font-size: 32px;
|
||||
}
|
||||
.ki-4x {
|
||||
font-size: 48px;
|
||||
}
|
||||
.ki-5x {
|
||||
font-size: 64px;
|
||||
}
|
||||
.ki-6x {
|
||||
font-size: 80px;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Використання у HTML
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="/assets/keenicons/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<i class="ki-solid ki-abstract-2 ki-xs" />
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@crux-kit/keenicon",
|
||||
"version": "1.3.1",
|
||||
"type": "module",
|
||||
"types": "src/index.ts",
|
||||
"private": true,
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./assets/styles.css": "./assets/styles.css"
|
||||
},
|
||||
"style": "assets/styles.css",
|
||||
"scripts": {
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
Open *demo.html* to see a list of all the glyphs in your font along with their codes/ligatures.
|
||||
|
||||
To use the generated font in desktop programs, you can install the TTF font. In order to copy the character associated with each icon, refer to the text box at the bottom right corner of each glyph in demo.html. The character inside this text box may be invisible; but it can still be copied. See this guide for more info: https://icomoon.io/docs/#local-fonts
|
||||
|
||||
You won't need any of the files located under the *demo-files* directory when including the generated font in your own projects.
|
||||
|
||||
You can import *selection.json* back to the IcoMoon app using the *Import Icons* button (or via Main Menu → Manage Projects) to retrieve your icon selection.
|
||||
@@ -0,0 +1,160 @@
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: sans-serif;
|
||||
font-size: 1em;
|
||||
line-height: 1.5;
|
||||
color: #555;
|
||||
background: #fff;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.5em;
|
||||
font-weight: normal;
|
||||
}
|
||||
small {
|
||||
font-size: 0.66666667em;
|
||||
}
|
||||
a {
|
||||
color: #e74c3c;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover,
|
||||
a:focus {
|
||||
box-shadow: 0 1px #e74c3c;
|
||||
}
|
||||
.bshadow0,
|
||||
input {
|
||||
box-shadow: inset 0 -2px #e7e7e7;
|
||||
}
|
||||
input:hover {
|
||||
box-shadow: inset 0 -2px #ccc;
|
||||
}
|
||||
input,
|
||||
fieldset {
|
||||
font-family: sans-serif;
|
||||
font-size: 1em;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
input {
|
||||
color: inherit;
|
||||
line-height: 1.5;
|
||||
height: 1.5em;
|
||||
padding: 0.25em 0;
|
||||
}
|
||||
input:focus {
|
||||
outline: none;
|
||||
box-shadow: inset 0 -2px #449fdb;
|
||||
}
|
||||
.glyph {
|
||||
font-size: 16px;
|
||||
width: 15em;
|
||||
padding-bottom: 1em;
|
||||
margin-right: 4em;
|
||||
margin-bottom: 1em;
|
||||
float: left;
|
||||
overflow: hidden;
|
||||
}
|
||||
.liga {
|
||||
width: 80%;
|
||||
width: calc(100% - 2.5em);
|
||||
}
|
||||
.talign-right {
|
||||
text-align: right;
|
||||
}
|
||||
.talign-center {
|
||||
text-align: center;
|
||||
}
|
||||
.bgc1 {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
.fgc1 {
|
||||
color: #999;
|
||||
}
|
||||
.fgc0 {
|
||||
color: #000;
|
||||
}
|
||||
p {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.mvm {
|
||||
margin-top: 0.75em;
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
.mtn {
|
||||
margin-top: 0;
|
||||
}
|
||||
.mtl,
|
||||
.mal {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
.mbl,
|
||||
.mal {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
.mal,
|
||||
.mhl {
|
||||
margin-left: 1.5em;
|
||||
margin-right: 1.5em;
|
||||
}
|
||||
.mhmm {
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
.mls {
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
.ptl {
|
||||
padding-top: 1.5em;
|
||||
}
|
||||
.pbs,
|
||||
.pvs {
|
||||
padding-bottom: 0.25em;
|
||||
}
|
||||
.pvs,
|
||||
.pts {
|
||||
padding-top: 0.25em;
|
||||
}
|
||||
.unit {
|
||||
float: left;
|
||||
}
|
||||
.unitRight {
|
||||
float: right;
|
||||
}
|
||||
.size1of2 {
|
||||
width: 50%;
|
||||
}
|
||||
.size1of1 {
|
||||
width: 100%;
|
||||
}
|
||||
.clearfix:before,
|
||||
.clearfix:after {
|
||||
content: ' ';
|
||||
display: table;
|
||||
}
|
||||
.clearfix:after {
|
||||
clear: both;
|
||||
}
|
||||
.hidden-true {
|
||||
display: none;
|
||||
}
|
||||
.textbox0 {
|
||||
width: 3em;
|
||||
background: #f1f1f1;
|
||||
padding: 0.25em 0.5em;
|
||||
line-height: 1.5;
|
||||
height: 1.5em;
|
||||
}
|
||||
#testDrive {
|
||||
display: block;
|
||||
padding-top: 24px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.fs0 {
|
||||
font-size: 16px;
|
||||
}
|
||||
.fs1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
if (!('boxShadow' in document.body.style)) {
|
||||
document.body.setAttribute('class', 'noBoxShadow');
|
||||
}
|
||||
|
||||
document.body.addEventListener('click', function (e) {
|
||||
var target = e.target;
|
||||
if (target.tagName === 'INPUT' && target.getAttribute('class').indexOf('liga') === -1) {
|
||||
target.select();
|
||||
}
|
||||
});
|
||||
|
||||
(function () {
|
||||
var fontSize = document.getElementById('fontSize'),
|
||||
testDrive = document.getElementById('testDrive'),
|
||||
testText = document.getElementById('testText');
|
||||
function updateTest() {
|
||||
testDrive.innerHTML = testText.value || String.fromCharCode(160);
|
||||
if (window.icomoonLiga) {
|
||||
window.icomoonLiga(testDrive);
|
||||
}
|
||||
}
|
||||
function updateSize() {
|
||||
testDrive.style.fontSize = fontSize.value + 'px';
|
||||
}
|
||||
fontSize.addEventListener('change', updateSize, false);
|
||||
testText.addEventListener('input', updateTest, false);
|
||||
testText.addEventListener('change', updateTest, false);
|
||||
updateSize();
|
||||
})();
|
||||
+12414
File diff suppressed because it is too large
Load Diff
+1109
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 803 KiB |
BIN
Binary file not shown.
BIN
Binary file not shown.
+14109
File diff suppressed because it is too large
Load Diff
+4931
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
Open *demo.html* to see a list of all the glyphs in your font along with their codes/ligatures.
|
||||
|
||||
To use the generated font in desktop programs, you can install the TTF font. In order to copy the character associated with each icon, refer to the text box at the bottom right corner of each glyph in demo.html. The character inside this text box may be invisible; but it can still be copied. See this guide for more info: https://icomoon.io/docs/#local-fonts
|
||||
|
||||
You won't need any of the files located under the *demo-files* directory when including the generated font in your own projects.
|
||||
|
||||
You can import *selection.json* back to the IcoMoon app using the *Import Icons* button (or via Main Menu → Manage Projects) to retrieve your icon selection.
|
||||
@@ -0,0 +1,160 @@
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: sans-serif;
|
||||
font-size: 1em;
|
||||
line-height: 1.5;
|
||||
color: #555;
|
||||
background: #fff;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.5em;
|
||||
font-weight: normal;
|
||||
}
|
||||
small {
|
||||
font-size: 0.66666667em;
|
||||
}
|
||||
a {
|
||||
color: #e74c3c;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover,
|
||||
a:focus {
|
||||
box-shadow: 0 1px #e74c3c;
|
||||
}
|
||||
.bshadow0,
|
||||
input {
|
||||
box-shadow: inset 0 -2px #e7e7e7;
|
||||
}
|
||||
input:hover {
|
||||
box-shadow: inset 0 -2px #ccc;
|
||||
}
|
||||
input,
|
||||
fieldset {
|
||||
font-family: sans-serif;
|
||||
font-size: 1em;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
input {
|
||||
color: inherit;
|
||||
line-height: 1.5;
|
||||
height: 1.5em;
|
||||
padding: 0.25em 0;
|
||||
}
|
||||
input:focus {
|
||||
outline: none;
|
||||
box-shadow: inset 0 -2px #449fdb;
|
||||
}
|
||||
.glyph {
|
||||
font-size: 16px;
|
||||
width: 15em;
|
||||
padding-bottom: 1em;
|
||||
margin-right: 4em;
|
||||
margin-bottom: 1em;
|
||||
float: left;
|
||||
overflow: hidden;
|
||||
}
|
||||
.liga {
|
||||
width: 80%;
|
||||
width: calc(100% - 2.5em);
|
||||
}
|
||||
.talign-right {
|
||||
text-align: right;
|
||||
}
|
||||
.talign-center {
|
||||
text-align: center;
|
||||
}
|
||||
.bgc1 {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
.fgc1 {
|
||||
color: #999;
|
||||
}
|
||||
.fgc0 {
|
||||
color: #000;
|
||||
}
|
||||
p {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.mvm {
|
||||
margin-top: 0.75em;
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
.mtn {
|
||||
margin-top: 0;
|
||||
}
|
||||
.mtl,
|
||||
.mal {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
.mbl,
|
||||
.mal {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
.mal,
|
||||
.mhl {
|
||||
margin-left: 1.5em;
|
||||
margin-right: 1.5em;
|
||||
}
|
||||
.mhmm {
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
.mls {
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
.ptl {
|
||||
padding-top: 1.5em;
|
||||
}
|
||||
.pbs,
|
||||
.pvs {
|
||||
padding-bottom: 0.25em;
|
||||
}
|
||||
.pvs,
|
||||
.pts {
|
||||
padding-top: 0.25em;
|
||||
}
|
||||
.unit {
|
||||
float: left;
|
||||
}
|
||||
.unitRight {
|
||||
float: right;
|
||||
}
|
||||
.size1of2 {
|
||||
width: 50%;
|
||||
}
|
||||
.size1of1 {
|
||||
width: 100%;
|
||||
}
|
||||
.clearfix:before,
|
||||
.clearfix:after {
|
||||
content: ' ';
|
||||
display: table;
|
||||
}
|
||||
.clearfix:after {
|
||||
clear: both;
|
||||
}
|
||||
.hidden-true {
|
||||
display: none;
|
||||
}
|
||||
.textbox0 {
|
||||
width: 3em;
|
||||
background: #f1f1f1;
|
||||
padding: 0.25em 0.5em;
|
||||
line-height: 1.5;
|
||||
height: 1.5em;
|
||||
}
|
||||
#testDrive {
|
||||
display: block;
|
||||
padding-top: 24px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.fs0 {
|
||||
font-size: 16px;
|
||||
}
|
||||
.fs1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
if (!('boxShadow' in document.body.style)) {
|
||||
document.body.setAttribute('class', 'noBoxShadow');
|
||||
}
|
||||
|
||||
document.body.addEventListener('click', function (e) {
|
||||
var target = e.target;
|
||||
if (target.tagName === 'INPUT' && target.getAttribute('class').indexOf('liga') === -1) {
|
||||
target.select();
|
||||
}
|
||||
});
|
||||
|
||||
(function () {
|
||||
var fontSize = document.getElementById('fontSize'),
|
||||
testDrive = document.getElementById('testDrive'),
|
||||
testText = document.getElementById('testText');
|
||||
function updateTest() {
|
||||
testDrive.innerHTML = testText.value || String.fromCharCode(160);
|
||||
if (window.icomoonLiga) {
|
||||
window.icomoonLiga(testDrive);
|
||||
}
|
||||
}
|
||||
function updateSize() {
|
||||
testDrive.style.fontSize = fontSize.value + 'px';
|
||||
}
|
||||
fontSize.addEventListener('change', updateSize, false);
|
||||
testText.addEventListener('input', updateTest, false);
|
||||
testText.addEventListener('change', updateTest, false);
|
||||
updateSize();
|
||||
})();
|
||||
+12360
File diff suppressed because it is too large
Load Diff
+1082
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 1.6 MiB |
BIN
Binary file not shown.
BIN
Binary file not shown.
+14054
File diff suppressed because one or more lines are too long
+4769
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
Open *demo.html* to see a list of all the glyphs in your font along with their codes/ligatures.
|
||||
|
||||
To use the generated font in desktop programs, you can install the TTF font. In order to copy the character associated with each icon, refer to the text box at the bottom right corner of each glyph in demo.html. The character inside this text box may be invisible; but it can still be copied. See this guide for more info: https://icomoon.io/docs/#local-fonts
|
||||
|
||||
You won't need any of the files located under the *demo-files* directory when including the generated font in your own projects.
|
||||
|
||||
You can import *selection.json* back to the IcoMoon app using the *Import Icons* button (or via Main Menu → Manage Projects) to retrieve your icon selection.
|
||||
@@ -0,0 +1,160 @@
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: sans-serif;
|
||||
font-size: 1em;
|
||||
line-height: 1.5;
|
||||
color: #555;
|
||||
background: #fff;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.5em;
|
||||
font-weight: normal;
|
||||
}
|
||||
small {
|
||||
font-size: 0.66666667em;
|
||||
}
|
||||
a {
|
||||
color: #e74c3c;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover,
|
||||
a:focus {
|
||||
box-shadow: 0 1px #e74c3c;
|
||||
}
|
||||
.bshadow0,
|
||||
input {
|
||||
box-shadow: inset 0 -2px #e7e7e7;
|
||||
}
|
||||
input:hover {
|
||||
box-shadow: inset 0 -2px #ccc;
|
||||
}
|
||||
input,
|
||||
fieldset {
|
||||
font-family: sans-serif;
|
||||
font-size: 1em;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
input {
|
||||
color: inherit;
|
||||
line-height: 1.5;
|
||||
height: 1.5em;
|
||||
padding: 0.25em 0;
|
||||
}
|
||||
input:focus {
|
||||
outline: none;
|
||||
box-shadow: inset 0 -2px #449fdb;
|
||||
}
|
||||
.glyph {
|
||||
font-size: 16px;
|
||||
width: 15em;
|
||||
padding-bottom: 1em;
|
||||
margin-right: 4em;
|
||||
margin-bottom: 1em;
|
||||
float: left;
|
||||
overflow: hidden;
|
||||
}
|
||||
.liga {
|
||||
width: 80%;
|
||||
width: calc(100% - 2.5em);
|
||||
}
|
||||
.talign-right {
|
||||
text-align: right;
|
||||
}
|
||||
.talign-center {
|
||||
text-align: center;
|
||||
}
|
||||
.bgc1 {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
.fgc1 {
|
||||
color: #999;
|
||||
}
|
||||
.fgc0 {
|
||||
color: #000;
|
||||
}
|
||||
p {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.mvm {
|
||||
margin-top: 0.75em;
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
.mtn {
|
||||
margin-top: 0;
|
||||
}
|
||||
.mtl,
|
||||
.mal {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
.mbl,
|
||||
.mal {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
.mal,
|
||||
.mhl {
|
||||
margin-left: 1.5em;
|
||||
margin-right: 1.5em;
|
||||
}
|
||||
.mhmm {
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
.mls {
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
.ptl {
|
||||
padding-top: 1.5em;
|
||||
}
|
||||
.pbs,
|
||||
.pvs {
|
||||
padding-bottom: 0.25em;
|
||||
}
|
||||
.pvs,
|
||||
.pts {
|
||||
padding-top: 0.25em;
|
||||
}
|
||||
.unit {
|
||||
float: left;
|
||||
}
|
||||
.unitRight {
|
||||
float: right;
|
||||
}
|
||||
.size1of2 {
|
||||
width: 50%;
|
||||
}
|
||||
.size1of1 {
|
||||
width: 100%;
|
||||
}
|
||||
.clearfix:before,
|
||||
.clearfix:after {
|
||||
content: ' ';
|
||||
display: table;
|
||||
}
|
||||
.clearfix:after {
|
||||
clear: both;
|
||||
}
|
||||
.hidden-true {
|
||||
display: none;
|
||||
}
|
||||
.textbox0 {
|
||||
width: 3em;
|
||||
background: #f1f1f1;
|
||||
padding: 0.25em 0.5em;
|
||||
line-height: 1.5;
|
||||
height: 1.5em;
|
||||
}
|
||||
#testDrive {
|
||||
display: block;
|
||||
padding-top: 24px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.fs0 {
|
||||
font-size: 16px;
|
||||
}
|
||||
.fs1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
if (!('boxShadow' in document.body.style)) {
|
||||
document.body.setAttribute('class', 'noBoxShadow');
|
||||
}
|
||||
|
||||
document.body.addEventListener('click', function (e) {
|
||||
var target = e.target;
|
||||
if (target.tagName === 'INPUT' && target.getAttribute('class').indexOf('liga') === -1) {
|
||||
target.select();
|
||||
}
|
||||
});
|
||||
|
||||
(function () {
|
||||
var fontSize = document.getElementById('fontSize'),
|
||||
testDrive = document.getElementById('testDrive'),
|
||||
testText = document.getElementById('testText');
|
||||
function updateTest() {
|
||||
testDrive.innerHTML = testText.value || String.fromCharCode(160);
|
||||
if (window.icomoonLiga) {
|
||||
window.icomoonLiga(testDrive);
|
||||
}
|
||||
}
|
||||
function updateSize() {
|
||||
testDrive.style.fontSize = fontSize.value + 'px';
|
||||
}
|
||||
fontSize.addEventListener('change', updateSize, false);
|
||||
testText.addEventListener('input', updateTest, false);
|
||||
testText.addEventListener('change', updateTest, false);
|
||||
updateSize();
|
||||
})();
|
||||
+11346
File diff suppressed because it is too large
Load Diff
+575
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 1.2 MiB |
BIN
Binary file not shown.
BIN
Binary file not shown.
+13054
File diff suppressed because one or more lines are too long
+1721
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
||||
/* KeenIcon Size Utilities */
|
||||
.ki-xs {
|
||||
font-size: 12px !important;
|
||||
line-height: 1;
|
||||
}
|
||||
.ki-sm {
|
||||
font-size: 14px !important;
|
||||
line-height: 1;
|
||||
}
|
||||
.ki-md {
|
||||
font-size: 16px !important;
|
||||
line-height: 1;
|
||||
} /* Base size */
|
||||
.ki-lg {
|
||||
font-size: 18px !important;
|
||||
line-height: 1;
|
||||
}
|
||||
.ki-xl {
|
||||
font-size: 20px !important;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Larger Display Sizes */
|
||||
.ki-2x {
|
||||
font-size: 24px !important;
|
||||
line-height: 1;
|
||||
}
|
||||
.ki-3x {
|
||||
font-size: 32px !important;
|
||||
line-height: 1;
|
||||
}
|
||||
.ki-4x {
|
||||
font-size: 40px !important;
|
||||
line-height: 1;
|
||||
}
|
||||
.ki-5x {
|
||||
font-size: 48px !important;
|
||||
line-height: 1;
|
||||
}
|
||||
.ki-6x {
|
||||
font-size: 64px !important;
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
Open *demo.html* to see a list of all the glyphs in your font along with their codes/ligatures.
|
||||
|
||||
To use the generated font in desktop programs, you can install the TTF font. In order to copy the character associated with each icon, refer to the text box at the bottom right corner of each glyph in demo.html. The character inside this text box may be invisible; but it can still be copied. See this guide for more info: https://icomoon.io/docs/#local-fonts
|
||||
|
||||
You won't need any of the files located under the *demo-files* directory when including the generated font in your own projects.
|
||||
|
||||
You can import *selection.json* back to the IcoMoon app using the *Import Icons* button (or via Main Menu → Manage Projects) to retrieve your icon selection.
|
||||
@@ -0,0 +1,160 @@
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: sans-serif;
|
||||
font-size: 1em;
|
||||
line-height: 1.5;
|
||||
color: #555;
|
||||
background: #fff;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.5em;
|
||||
font-weight: normal;
|
||||
}
|
||||
small {
|
||||
font-size: 0.66666667em;
|
||||
}
|
||||
a {
|
||||
color: #e74c3c;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover,
|
||||
a:focus {
|
||||
box-shadow: 0 1px #e74c3c;
|
||||
}
|
||||
.bshadow0,
|
||||
input {
|
||||
box-shadow: inset 0 -2px #e7e7e7;
|
||||
}
|
||||
input:hover {
|
||||
box-shadow: inset 0 -2px #ccc;
|
||||
}
|
||||
input,
|
||||
fieldset {
|
||||
font-family: sans-serif;
|
||||
font-size: 1em;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
input {
|
||||
color: inherit;
|
||||
line-height: 1.5;
|
||||
height: 1.5em;
|
||||
padding: 0.25em 0;
|
||||
}
|
||||
input:focus {
|
||||
outline: none;
|
||||
box-shadow: inset 0 -2px #449fdb;
|
||||
}
|
||||
.glyph {
|
||||
font-size: 16px;
|
||||
width: 15em;
|
||||
padding-bottom: 1em;
|
||||
margin-right: 4em;
|
||||
margin-bottom: 1em;
|
||||
float: left;
|
||||
overflow: hidden;
|
||||
}
|
||||
.liga {
|
||||
width: 80%;
|
||||
width: calc(100% - 2.5em);
|
||||
}
|
||||
.talign-right {
|
||||
text-align: right;
|
||||
}
|
||||
.talign-center {
|
||||
text-align: center;
|
||||
}
|
||||
.bgc1 {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
.fgc1 {
|
||||
color: #999;
|
||||
}
|
||||
.fgc0 {
|
||||
color: #000;
|
||||
}
|
||||
p {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.mvm {
|
||||
margin-top: 0.75em;
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
.mtn {
|
||||
margin-top: 0;
|
||||
}
|
||||
.mtl,
|
||||
.mal {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
.mbl,
|
||||
.mal {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
.mal,
|
||||
.mhl {
|
||||
margin-left: 1.5em;
|
||||
margin-right: 1.5em;
|
||||
}
|
||||
.mhmm {
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
.mls {
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
.ptl {
|
||||
padding-top: 1.5em;
|
||||
}
|
||||
.pbs,
|
||||
.pvs {
|
||||
padding-bottom: 0.25em;
|
||||
}
|
||||
.pvs,
|
||||
.pts {
|
||||
padding-top: 0.25em;
|
||||
}
|
||||
.unit {
|
||||
float: left;
|
||||
}
|
||||
.unitRight {
|
||||
float: right;
|
||||
}
|
||||
.size1of2 {
|
||||
width: 50%;
|
||||
}
|
||||
.size1of1 {
|
||||
width: 100%;
|
||||
}
|
||||
.clearfix:before,
|
||||
.clearfix:after {
|
||||
content: ' ';
|
||||
display: table;
|
||||
}
|
||||
.clearfix:after {
|
||||
clear: both;
|
||||
}
|
||||
.hidden-true {
|
||||
display: none;
|
||||
}
|
||||
.textbox0 {
|
||||
width: 3em;
|
||||
background: #f1f1f1;
|
||||
padding: 0.25em 0.5em;
|
||||
line-height: 1.5;
|
||||
height: 1.5em;
|
||||
}
|
||||
#testDrive {
|
||||
display: block;
|
||||
padding-top: 24px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.fs0 {
|
||||
font-size: 16px;
|
||||
}
|
||||
.fs1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
if (!('boxShadow' in document.body.style)) {
|
||||
document.body.setAttribute('class', 'noBoxShadow');
|
||||
}
|
||||
|
||||
document.body.addEventListener('click', function (e) {
|
||||
var target = e.target;
|
||||
if (target.tagName === 'INPUT' && target.getAttribute('class').indexOf('liga') === -1) {
|
||||
target.select();
|
||||
}
|
||||
});
|
||||
|
||||
(function () {
|
||||
var fontSize = document.getElementById('fontSize'),
|
||||
testDrive = document.getElementById('testDrive'),
|
||||
testText = document.getElementById('testText');
|
||||
function updateTest() {
|
||||
testDrive.innerHTML = testText.value || String.fromCharCode(160);
|
||||
if (window.icomoonLiga) {
|
||||
window.icomoonLiga(testDrive);
|
||||
}
|
||||
}
|
||||
function updateSize() {
|
||||
testDrive.style.fontSize = fontSize.value + 'px';
|
||||
}
|
||||
fontSize.addEventListener('change', updateSize, false);
|
||||
testText.addEventListener('input', updateTest, false);
|
||||
testText.addEventListener('change', updateTest, false);
|
||||
updateSize();
|
||||
})();
|
||||
+11346
File diff suppressed because it is too large
Load Diff
+575
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 878 KiB |
Binary file not shown.
BIN
Binary file not shown.
+13048
File diff suppressed because one or more lines are too long
+1721
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
@import './duotone/style.css';
|
||||
@import './outline/style.css';
|
||||
@import './filled/style.css';
|
||||
@import './solid/style.css';
|
||||
|
||||
@import './sizing.css';
|
||||
Executable
+2
@@ -0,0 +1,2 @@
|
||||
export * from './types';
|
||||
export * from './keenicon';
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { forwardRef } from 'react';
|
||||
import type { KeeniconsProps } from './types';
|
||||
|
||||
function joinClasses(...args: string[]): string {
|
||||
return args.join(' ');
|
||||
}
|
||||
|
||||
export const KeenIcon = forwardRef<HTMLElement, KeeniconsProps>((props, ref) => {
|
||||
const { icon, variant = 'filled', className = '', size = 'md', ...rest } = props;
|
||||
|
||||
return (
|
||||
<i
|
||||
ref={ref}
|
||||
{...rest}
|
||||
className={joinClasses(`ki-${variant}`, `ki-${icon}`, `ki-${size}`, className)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
KeenIcon.displayName = 'KeenIcon';
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
export type KeeniconVariant = 'duotone' | 'filled' | 'solid' | 'outline';
|
||||
export type KeeniconSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2x' | '3x' | '4x' | '5x' | '6x';
|
||||
|
||||
export interface KeeniconsProps {
|
||||
icon: string;
|
||||
variant?: KeeniconVariant;
|
||||
className?: string;
|
||||
size?: KeeniconSize;
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2023", "DOM"],
|
||||
"jsx": "react-jsx",
|
||||
"skipLibCheck": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"composite": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user