You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/soma/components/words/engine/document.ts

179 lines
4.9 KiB

Build TextArea component + fix locale propagation bug across soma providers Major work in this session: 1. NumberField demo refactored to expose Intl.NumberFormatOptions live - 9 canonical presets (currency/unit/percent/compact/plain) - Locale/currency/unit lists derived from $libs/langs, $libs/currency, $format/units (no more hardcoded arrays) - Default locale from uix.format.numbers.getLocale() 2. Bug fix: locale prop ignored when soma.format service is active - soma/NumberField: 5 bypass points via new `hasOwnLocale` getter (decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue) - soma/Calendar: dateOrder respects locale prop (matches DateField precedence) - soma/RangeCalendar: same dateOrder fix - When the consumer passes `locale` prop, soma.nums/dates is bypassed and Intl is used directly with the prop locale 3. TextArea — new component, full 4-layer stack - morfo: 3 parts (provider/input/count), 3 events (commit-input/commit-submit/shift-count-overflow), APG textbox - soma: TextAreaProvider with autosize (scrollHeight measurement), submitOn modes (false|enter|shift+enter|mod+enter), maxLength soft clamp, Field integration (OR-merge), counter overflow tracking - eidos: 5 sizes × 3 variants × 8 colors, autosize suppresses CSS resize - sema: commit-submit + shift-count-overflow perceptual cues - demo: canonical 6-tab depth, all props exposed as live controls - PrimitiveTextareaAttributes added to soma/types/html.ts 4. Also includes accumulated unreleased work from prior sessions: - Eidos ports: announce, clipboard, drag-drop, feed, table, tree-grid, tree-view, virtual-grid, virtual-list, words - Their canonical demos under web/routes/uix/components/ - Semantic events defined for utility morfos (announce/clipboard/virtual-list/virtual-grid) - Tree indent guides fixed (TreeView ::before, TreeGrid stacked gradients) - Sidebar reorganized with Data + Utilities groups - Eidos words content layer and primitives Reference library survey performed before TextArea API design (Mantine, Radix Themes, Chakra v3, React Aria, Ark UI, Bits UI, shadcn, MUI, Prime). Decision documented: no separate CurrencyField/UnitField — single NumberField with formatOptions is the canon (matches all surveyed headless libs except PrimeReact's mode enum, which still uses 1 component). svelte-check: 0 errors in modified/created files. See CONTINUE.md for full session notes and next steps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
export const WORDS_DOCUMENT_VERSION = 1;
export type WordsMark = 'bold' | 'italic' | 'underline' | 'strike' | 'code';
export type WordsBlockType = 'paragraph' | 'heading' | 'quote' | 'list';
export type WordsListKind = 'ordered' | 'unordered' | 'check';
export type WordsHeadingLevel = 1 | 2 | 3;
export interface WordsDocument {
readonly version: typeof WORDS_DOCUMENT_VERSION;
readonly children: readonly WordsBlock[];
}
export type WordsBlock = WordsParagraphBlock | WordsHeadingBlock | WordsQuoteBlock | WordsListBlock;
export interface WordsParagraphBlock {
readonly type: 'paragraph';
readonly children: readonly WordsInline[];
}
export interface WordsHeadingBlock {
readonly type: 'heading';
readonly level: WordsHeadingLevel;
readonly children: readonly WordsInline[];
}
export interface WordsQuoteBlock {
readonly type: 'quote';
readonly children: readonly WordsInline[];
}
export interface WordsListBlock {
readonly type: 'list';
readonly kind: WordsListKind;
readonly children: readonly WordsListItem[];
}
export interface WordsListItem {
readonly type: 'list-item';
readonly checked?: boolean;
readonly children: readonly WordsInline[];
}
export type WordsInline = WordsText | WordsLink;
export interface WordsText {
readonly type: 'text';
readonly text: string;
readonly marks?: readonly WordsMark[];
}
export interface WordsLink {
readonly type: 'link';
readonly href: string;
readonly title?: string;
readonly children: readonly WordsInline[];
}
export const WORDS_MARKS = [
'bold',
'italic',
'underline',
'strike',
'code'
] as const satisfies readonly WordsMark[];
export const WORDS_LIST_KINDS = [
'ordered',
'unordered',
'check'
] as const satisfies readonly WordsListKind[];
export const WORDS_HEADING_LEVELS = [1, 2, 3] as const satisfies readonly WordsHeadingLevel[];
export function createEmptyWordsDocument(): WordsDocument {
return {
version: WORDS_DOCUMENT_VERSION,
children: [createParagraph()]
};
}
export function createParagraph(
children: readonly WordsInline[] = [createText('')]
): WordsParagraphBlock {
return { type: 'paragraph', children };
}
export function createHeading(
level: WordsHeadingLevel,
children: readonly WordsInline[] = [createText('')]
): WordsHeadingBlock {
return { type: 'heading', level, children };
}
export function createQuote(children: readonly WordsInline[] = [createText('')]): WordsQuoteBlock {
return { type: 'quote', children };
}
export function createList(
kind: WordsListKind,
children: readonly WordsListItem[] = [createListItem()]
): WordsListBlock {
return { type: 'list', kind, children };
}
export function createListItem(
children: readonly WordsInline[] = [createText('')],
checked = false
): WordsListItem {
return { type: 'list-item', ...(checked ? { checked } : {}), children };
}
export function createText(text: string, marks: readonly WordsMark[] = []): WordsText {
const normalizedMarks = normalizeMarks(marks);
return {
type: 'text',
text,
...(normalizedMarks.length ? { marks: normalizedMarks } : {})
};
}
export function createLink(
href: string,
children: readonly WordsInline[],
title?: string
): WordsLink {
return {
type: 'link',
href,
...(title ? { title } : {}),
children
};
}
export function normalizeMarks(marks: readonly WordsMark[] | undefined): readonly WordsMark[] {
if (!marks?.length) return [];
const order = new Map<WordsMark, number>(WORDS_MARKS.map((mark, index) => [mark, index]));
return Array.from(new Set(marks)).sort((a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0));
}
export function sameMarks(
a: readonly WordsMark[] | undefined,
b: readonly WordsMark[] | undefined
): boolean {
const left = normalizeMarks(a);
const right = normalizeMarks(b);
if (left.length !== right.length) return false;
return left.every((mark, index) => mark === right[index]);
}
export function isWordsMark(value: unknown): value is WordsMark {
return typeof value === 'string' && (WORDS_MARKS as readonly string[]).includes(value);
}
export function isWordsHeadingLevel(value: unknown): value is WordsHeadingLevel {
return typeof value === 'number' && (WORDS_HEADING_LEVELS as readonly number[]).includes(value);
}
export function isWordsListKind(value: unknown): value is WordsListKind {
return typeof value === 'string' && (WORDS_LIST_KINDS as readonly string[]).includes(value);
}
export function isTextInline(inline: WordsInline): inline is WordsText {
return inline.type === 'text';
}
export function isLinkInline(inline: WordsInline): inline is WordsLink {
return inline.type === 'link';
}
export function getBlockText(block: WordsBlock): string {
if (block.type === 'list') {
return block.children.map((item) => item.children.map(getInlineText).join('')).join('\n');
}
return block.children.map(getInlineText).join('');
}
export function getInlineText(inline: WordsInline): string {
if (inline.type === 'text') return inline.text;
return inline.children.map(getInlineText).join('');
}

Powered by TurnKey Linux.