@mosetta/ui 0.0.0-stage → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -0
- package/LICENSE +21 -0
- package/README.md +4 -2
- package/css/base.css +109 -0
- package/css/button.css +264 -0
- package/css/card.css +174 -0
- package/css/form.css +107 -0
- package/css/inline-field.css +195 -0
- package/css/inline.css +46 -0
- package/css/input.css +163 -0
- package/css/stack.css +39 -0
- package/css/styler.css +134 -0
- package/css/typography.css +136 -0
- package/package.json +35 -3
- package/scripts/buildCss.mjs +73 -0
- package/src/cn.ts +28 -0
- package/src/components/button.tsx +46 -0
- package/src/components/card.tsx +23 -0
- package/src/components/form.tsx +31 -0
- package/src/components/inline.tsx +30 -0
- package/src/components/inlineField.tsx +118 -0
- package/src/components/input.tsx +18 -0
- package/src/components/mosaic.tsx +52 -0
- package/src/components/stack.tsx +42 -0
- package/src/components/styler.tsx +229 -0
- package/src/components/typography.tsx +98 -0
- package/src/dockArrangement.ts +212 -0
- package/src/dockLayout.ts +139 -0
- package/src/inline.ts +103 -0
- package/src/ui.ts +54 -0
- package/src/variants.ts +28 -0
- package/styles/theme.css +107 -0
- package/styles/tw.css +36 -0
- package/tsconfig.json +7 -0
package/src/inline.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Inline markup of a single string setting — the one concession composition makes to writing.
|
|
2
|
+
// An article is blocks (Heading, Text, List, Quote); a bold word INSIDE a paragraph is not a
|
|
3
|
+
// block, so a string carries a tiny, dependency-free grammar: **bold**, *italic*, `code`,
|
|
4
|
+
// [label](url). Nothing else: no headings, no images, no HTML — those are blocks or nothing.
|
|
5
|
+
// This module is the PARSER (pure data); the react rendering lives beside the typography.
|
|
6
|
+
|
|
7
|
+
export type TInlineNode
|
|
8
|
+
= | { readonly kind: 'text'; readonly text: string }
|
|
9
|
+
| { readonly kind: 'bold'; readonly children: readonly TInlineNode[] }
|
|
10
|
+
| { readonly kind: 'italic'; readonly children: readonly TInlineNode[] }
|
|
11
|
+
| { readonly kind: 'code'; readonly text: string }
|
|
12
|
+
| { readonly kind: 'link'; readonly href: string; readonly children: readonly TInlineNode[] };
|
|
13
|
+
|
|
14
|
+
// a link may only lead somewhere a page may lead: web, mail, same-site paths and anchors —
|
|
15
|
+
// never script
|
|
16
|
+
const SAFE_HREF_RE = /^(https?:\/\/|mailto:|\/|#|\.\/|\.\.\/)/i;
|
|
17
|
+
|
|
18
|
+
/** whether an href is one the renderer will honour (an unsafe one renders as plain text) */
|
|
19
|
+
export const isSafeHref = (href: string): boolean => SAFE_HREF_RE.test(href.trim());
|
|
20
|
+
|
|
21
|
+
/** a marker that reads as literal text: `\*` `\_` `\[` `` \` `` `\\` */
|
|
22
|
+
const ESCAPABLE = new Set(['*', '_', '[', ']', '`', '\\']);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* One recursive descent over the string; `until` is the delimiter that closes the current run
|
|
26
|
+
* (`**`, `*`, `_`, `]`) — a run that never closes is plain text, exactly as typed.
|
|
27
|
+
*/
|
|
28
|
+
function parseRun(src: string, until: string | null): { nodes: TInlineNode[]; end: number; closed: boolean } {
|
|
29
|
+
const nodes: TInlineNode[] = [];
|
|
30
|
+
let text = '';
|
|
31
|
+
const flush = () => {
|
|
32
|
+
if (text) nodes.push({ kind: 'text', text });
|
|
33
|
+
text = '';
|
|
34
|
+
};
|
|
35
|
+
let i = 0;
|
|
36
|
+
while (i < src.length) {
|
|
37
|
+
const ch = src[i];
|
|
38
|
+
if (until && src.startsWith(until, i)) {
|
|
39
|
+
flush();
|
|
40
|
+
return { nodes, end: i + until.length, closed: true };
|
|
41
|
+
}
|
|
42
|
+
if (ch === '\\' && i + 1 < src.length && ESCAPABLE.has(src[i + 1])) {
|
|
43
|
+
text += src[i + 1];
|
|
44
|
+
i += 2;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (ch === '`') {
|
|
48
|
+
const close = src.indexOf('`', i + 1);
|
|
49
|
+
if (close > i + 1) {
|
|
50
|
+
flush();
|
|
51
|
+
nodes.push({ kind: 'code', text: src.slice(i + 1, close) });
|
|
52
|
+
i = close + 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (src.startsWith('**', i) || ch === '*' || ch === '_') {
|
|
57
|
+
const mark = src.startsWith('**', i) ? '**' : ch;
|
|
58
|
+
const inner = parseRun(src.slice(i + mark.length), mark);
|
|
59
|
+
// an empty pair (** **) or an unclosed one is just characters
|
|
60
|
+
if (inner.closed && inner.nodes.length) {
|
|
61
|
+
flush();
|
|
62
|
+
nodes.push({ kind: mark === '**' ? 'bold' : 'italic', children: inner.nodes });
|
|
63
|
+
i += mark.length + inner.end;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (ch === '[') {
|
|
68
|
+
const label = parseRun(src.slice(i + 1), ']');
|
|
69
|
+
const afterLabel = i + 1 + label.end;
|
|
70
|
+
if (label.closed && src[afterLabel] === '(') {
|
|
71
|
+
const close = src.indexOf(')', afterLabel + 1);
|
|
72
|
+
if (close > afterLabel + 1) {
|
|
73
|
+
const href = src.slice(afterLabel + 1, close).trim();
|
|
74
|
+
flush();
|
|
75
|
+
nodes.push({ kind: 'link', href, children: label.nodes.length ? label.nodes : [{ kind: 'text', text: href }] });
|
|
76
|
+
i = close + 1;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
text += ch;
|
|
82
|
+
i += 1;
|
|
83
|
+
}
|
|
84
|
+
flush();
|
|
85
|
+
return { nodes, end: src.length, closed: false };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function parseInline(text: string): TInlineNode[] {
|
|
89
|
+
return parseRun(text, null).nodes;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** the markup stripped: what a search index, a title attribute or a translator's segment sees */
|
|
93
|
+
export function plainTextOf(nodes: readonly TInlineNode[]): string {
|
|
94
|
+
return nodes.map((n) => {
|
|
95
|
+
switch (n.kind) {
|
|
96
|
+
case 'text': return n.text;
|
|
97
|
+
case 'code': return n.text;
|
|
98
|
+
case 'bold': case 'italic': return plainTextOf(n.children);
|
|
99
|
+
case 'link': return plainTextOf(n.children);
|
|
100
|
+
default: return '';
|
|
101
|
+
}
|
|
102
|
+
}).join('');
|
|
103
|
+
}
|
package/src/ui.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// @mosetta/ui — the shadcn-style base library. Bundled into consumers as a
|
|
2
|
+
// dependency artifact; NO css import here (dependency artifacts carry no css) — a consuming
|
|
3
|
+
// MODULE imports '@mosetta/ui/base.css' + per-component slices ('@mosetta/ui/button.css', ...)
|
|
4
|
+
// — those travel as SHARED dependency-css artifacts, not in the module's scoped css.
|
|
5
|
+
// Styling contract: render inside a `dui-root`-classed element (theme vars + scoped preflight
|
|
6
|
+
// attach there; the generated utilities are `dui:`-prefixed and var-driven).
|
|
7
|
+
export { cn, type ClassValue } from './cn';
|
|
8
|
+
export { variants, type IVariantsConfig, type VariantProps } from './variants';
|
|
9
|
+
export { Button, buttonVariants, type IButtonProps } from './components/button';
|
|
10
|
+
export { Input, type IInputProps } from './components/input';
|
|
11
|
+
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './components/card';
|
|
12
|
+
export {
|
|
13
|
+
Heading,
|
|
14
|
+
Text,
|
|
15
|
+
List,
|
|
16
|
+
ListItem,
|
|
17
|
+
Blockquote,
|
|
18
|
+
type IHeadingProps,
|
|
19
|
+
type ITextProps,
|
|
20
|
+
type IListProps,
|
|
21
|
+
type IBlockquoteProps,
|
|
22
|
+
} from './components/typography';
|
|
23
|
+
export { Inline } from './components/inline';
|
|
24
|
+
export { InlineField, type IInlineFieldProps } from './components/inlineField';
|
|
25
|
+
export { parseInline, plainTextOf, isSafeHref, type TInlineNode } from './inline';
|
|
26
|
+
export { Stack, stackVariants, type IStackProps } from './components/stack';
|
|
27
|
+
export { editorForm, FormLabel } from './components/form';
|
|
28
|
+
export { Mosaic, type IMosaicProps } from './components/mosaic';
|
|
29
|
+
export { StylerForm, type IStylerFormProps, type IStylerPart } from './components/styler';
|
|
30
|
+
export {
|
|
31
|
+
serializeArrangement,
|
|
32
|
+
parseArrangement,
|
|
33
|
+
COMPONENT_TAB_PREFIX,
|
|
34
|
+
EMPTY_TAB_PREFIX,
|
|
35
|
+
type IArrangedTab,
|
|
36
|
+
type IArrangedGroup,
|
|
37
|
+
type IArrangedBranch,
|
|
38
|
+
type TArrangedNode,
|
|
39
|
+
type IArrangement,
|
|
40
|
+
} from './dockArrangement';
|
|
41
|
+
export {
|
|
42
|
+
DockLayout,
|
|
43
|
+
DockTab,
|
|
44
|
+
DockTabRow,
|
|
45
|
+
DOCK_LAYOUT_TOKEN,
|
|
46
|
+
DOCK_TAB_TOKEN,
|
|
47
|
+
DOCK_TAB_ROW_TOKEN,
|
|
48
|
+
type IDockComponent,
|
|
49
|
+
type IDockOpenOptions,
|
|
50
|
+
type IDockOpenable,
|
|
51
|
+
type IDockTabInfo,
|
|
52
|
+
type IDockWant,
|
|
53
|
+
type TDockSide,
|
|
54
|
+
} from './dockLayout';
|
package/src/variants.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Mini-cva: the shadcn variant convention without the class-variance-authority dependency.
|
|
2
|
+
// A component declares base classes + named variant axes + defaults; the returned builder picks
|
|
3
|
+
// one class set per axis and appends the caller's className last.
|
|
4
|
+
import { cn } from './cn';
|
|
5
|
+
|
|
6
|
+
type VariantAxes = Record<string, Record<string, string>>;
|
|
7
|
+
|
|
8
|
+
type VariantProps<V extends VariantAxes> = {
|
|
9
|
+
[K in keyof V]?: keyof V[K];
|
|
10
|
+
} & { className?: string };
|
|
11
|
+
|
|
12
|
+
export interface IVariantsConfig<V extends VariantAxes> {
|
|
13
|
+
readonly base?: string;
|
|
14
|
+
readonly variants: V;
|
|
15
|
+
readonly defaultVariants?: { [K in keyof V]?: keyof V[K] };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function variants<V extends VariantAxes>({ base, variants: axes, defaultVariants }: IVariantsConfig<V>) {
|
|
19
|
+
return (props?: VariantProps<V>): string => {
|
|
20
|
+
const picked = Object.keys(axes).map((axis) => {
|
|
21
|
+
const choice = props?.[axis] ?? defaultVariants?.[axis];
|
|
22
|
+
return choice != null ? axes[axis][choice as string] : undefined;
|
|
23
|
+
});
|
|
24
|
+
return cn(base, ...picked, props?.className);
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type { VariantProps };
|
package/styles/theme.css
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/* @mosetta/ui theme + SCOPED preflight . Everything lives under .dui-root —
|
|
2
|
+
* NO global selectors: the editor shares one document with the page being edited, and a global
|
|
3
|
+
* reset would pollute foreign protocol space. Components must render inside a .dui-root element. */
|
|
4
|
+
|
|
5
|
+
.dui-root {
|
|
6
|
+
/* shadcn-style palette (light), dui-prefixed vars — the tailwind theme maps color utilities here.
|
|
7
|
+
Every value comes from the THEME ROLE around the block when there is one (@mosetta/core
|
|
8
|
+
theme.ts: `--theme-<role>`), and is the kit's own look where there is none — the editor's own
|
|
9
|
+
UI wears no theme, so it never changes with the page being edited. */
|
|
10
|
+
--dui-background: var(--theme-background, #ffffff);
|
|
11
|
+
--dui-foreground: var(--theme-text, #09090b);
|
|
12
|
+
--dui-card: var(--theme-background, #ffffff);
|
|
13
|
+
--dui-card-foreground: var(--theme-text, #09090b);
|
|
14
|
+
--dui-primary: var(--theme-primary, #18181b);
|
|
15
|
+
--dui-primary-foreground: var(--theme-primary-text, #fafafa);
|
|
16
|
+
--dui-secondary: var(--theme-muted, #f4f4f5);
|
|
17
|
+
--dui-secondary-foreground: var(--theme-text, #18181b);
|
|
18
|
+
--dui-muted: var(--theme-muted, #f4f4f5);
|
|
19
|
+
--dui-muted-foreground: var(--theme-muted-text, #71717a);
|
|
20
|
+
--dui-accent: var(--theme-muted, #f4f4f5);
|
|
21
|
+
--dui-accent-foreground: var(--theme-text, #18181b);
|
|
22
|
+
--dui-destructive: #dc2626;
|
|
23
|
+
--dui-destructive-foreground: #fafafa;
|
|
24
|
+
--dui-border: var(--theme-border, #e4e4e7);
|
|
25
|
+
--dui-input: var(--theme-border, #e4e4e7);
|
|
26
|
+
--dui-ring: var(--theme-primary, #18181b);
|
|
27
|
+
--dui-radius: var(--theme-radius, 0.5rem);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* The INHERITED defaults, at zero specificity on purpose. A protocol module has no shared
|
|
31
|
+
* ancestor to put the palette on, so every one of them wears .dui-root ITSELF — and then a rule
|
|
32
|
+
* here and a utility class on the same element carry the same weight, leaving the winner to
|
|
33
|
+
* whichever stylesheet the page happened to load last. That is how a dark button ended up with
|
|
34
|
+
* dark text. `:where` takes this rule out of the contest: it is what applies when nothing else
|
|
35
|
+
* says otherwise, which is all a default should ever be. */
|
|
36
|
+
:where(.dui-root) {
|
|
37
|
+
color: var(--dui-foreground);
|
|
38
|
+
font-family: var(--theme-font-body, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif);
|
|
39
|
+
font-size: 14px;
|
|
40
|
+
line-height: 1.5;
|
|
41
|
+
-webkit-font-smoothing: antialiased;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* headings take the theme's heading font — on a block that IS a heading and on one inside a block;
|
|
45
|
+
* zero weight like every default here */
|
|
46
|
+
:where(.dui-root):where(h1, h2, h3, h4, h5, h6),
|
|
47
|
+
:where(.dui-root) :where(h1, h2, h3, h4, h5, h6) {
|
|
48
|
+
font-family: var(--theme-font-heading, inherit);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* The mini-preflight: the subset of tailwind's reset our primitives rely on, and the SAME rule as
|
|
52
|
+
* above applies to it — a reset is a default, and a default must never outweigh a utility. Written
|
|
53
|
+
* with an ancestor class it would carry MORE weight than the utilities it is meant to yield to:
|
|
54
|
+
* `.dui-root h2 { font-size: inherit }` beats `.dui:text-3xl`, and a heading inside a card would
|
|
55
|
+
* quietly lose its size. `:where` keeps the whole layer at zero. */
|
|
56
|
+
:where(.dui-root) *,
|
|
57
|
+
:where(.dui-root) ::before,
|
|
58
|
+
:where(.dui-root) ::after {
|
|
59
|
+
box-sizing: border-box;
|
|
60
|
+
border-width: 0;
|
|
61
|
+
border-style: solid;
|
|
62
|
+
border-color: var(--dui-border);
|
|
63
|
+
margin: 0;
|
|
64
|
+
padding: 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
:where(.dui-root) :where(h1, h2, h3, h4, p) {
|
|
68
|
+
font-size: inherit;
|
|
69
|
+
font-weight: inherit;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
:where(.dui-root) :where(button, input, select, textarea) {
|
|
73
|
+
font: inherit;
|
|
74
|
+
color: inherit;
|
|
75
|
+
background: transparent;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
:where(.dui-root) :where(button) {
|
|
79
|
+
appearance: none;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* The native select arrow sits wherever the browser draws it — flush against the right border,
|
|
83
|
+
* and no padding moves it. The arrow is ours instead: an inset chevron that the control's own
|
|
84
|
+
* right padding (editorForm.select) keeps clear of the text. */
|
|
85
|
+
:where(.dui-root) :where(select) {
|
|
86
|
+
appearance: none;
|
|
87
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 6l4 4 4-4' fill='none' stroke='%2371717a' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
88
|
+
background-repeat: no-repeat;
|
|
89
|
+
background-position: right 0.75rem center;
|
|
90
|
+
background-size: 1rem 1rem;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
:where(.dui-root) :where(img, svg) {
|
|
94
|
+
display: block;
|
|
95
|
+
max-width: 100%;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
:where(.dui-root) :where(a) {
|
|
99
|
+
color: inherit;
|
|
100
|
+
text-decoration: inherit;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* a link inside text takes the theme's accent; with no theme around, the colour of its text — LAST
|
|
104
|
+
* on purpose: it weighs as little as the reset of every anchor above, so it must come after it */
|
|
105
|
+
:where(.dui-root) :where(a.dui-inline-link) {
|
|
106
|
+
color: var(--theme-accent, currentColor);
|
|
107
|
+
}
|
package/styles/tw.css
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/* Tailwind v4 entry for the OFFLINE css build (scripts/buildCss.mjs — the engine never enters the
|
|
2
|
+
* CLI module pipeline). Theme tokens + utilities, `dui` prefix; preflight is NOT
|
|
3
|
+
* imported — our theme.css ships its own scoped mini-reset, and buildCss rescopes the token
|
|
4
|
+
* :root/:host blocks under .dui-root. */
|
|
5
|
+
@import 'tailwindcss/theme.css' prefix(dui);
|
|
6
|
+
@import 'tailwindcss/utilities.css' prefix(dui) source(none);
|
|
7
|
+
|
|
8
|
+
/* scan the library sources for dui:* classes (consumers' extra looks go into their *.styles.css) */
|
|
9
|
+
@source '../src';
|
|
10
|
+
|
|
11
|
+
/* shadcn semantic colors → our .dui-root-scoped vars; `inline` collapses the indirection so
|
|
12
|
+
* utilities reference var(--dui-*) directly */
|
|
13
|
+
@theme inline {
|
|
14
|
+
--color-background: var(--dui-background);
|
|
15
|
+
--color-foreground: var(--dui-foreground);
|
|
16
|
+
--color-card: var(--dui-card);
|
|
17
|
+
--color-card-foreground: var(--dui-card-foreground);
|
|
18
|
+
--color-primary: var(--dui-primary);
|
|
19
|
+
--color-primary-foreground: var(--dui-primary-foreground);
|
|
20
|
+
--color-secondary: var(--dui-secondary);
|
|
21
|
+
--color-secondary-foreground: var(--dui-secondary-foreground);
|
|
22
|
+
--color-muted: var(--dui-muted);
|
|
23
|
+
--color-muted-foreground: var(--dui-muted-foreground);
|
|
24
|
+
--color-accent: var(--dui-accent);
|
|
25
|
+
--color-accent-foreground: var(--dui-accent-foreground);
|
|
26
|
+
--color-destructive: var(--dui-destructive);
|
|
27
|
+
--color-destructive-foreground: var(--dui-destructive-foreground);
|
|
28
|
+
--color-border: var(--dui-border);
|
|
29
|
+
--color-input: var(--dui-input);
|
|
30
|
+
--color-ring: var(--dui-ring);
|
|
31
|
+
/* corners follow the theme's radius: the defaults below are tailwind's own sizes for 0.5rem */
|
|
32
|
+
--radius-sm: calc(var(--dui-radius) - 4px);
|
|
33
|
+
--radius-md: calc(var(--dui-radius) - 2px);
|
|
34
|
+
--radius-lg: var(--dui-radius);
|
|
35
|
+
--radius-xl: calc(var(--dui-radius) + 4px);
|
|
36
|
+
}
|