feat: themes
This commit is contained in:
@@ -21,6 +21,7 @@ from uframe.ir import (
|
|||||||
IRNode, Field, Password, Radio, Checkbox, FormButton,
|
IRNode, Field, Password, Radio, Checkbox, FormButton,
|
||||||
Source, IfBlock, ForLoop, OnSubmit, StateDecl, CacheControl,
|
Source, IfBlock, ForLoop, OnSubmit, StateDecl, CacheControl,
|
||||||
)
|
)
|
||||||
|
from uframe.themes import get_theme, ThemeDef
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -79,7 +80,7 @@ def _micron_form_line(node: IRNode) -> str:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def compile(source: str, width: int = 64) -> CompileResult:
|
def compile(source: str, width: int = 64, theme: str = "") -> CompileResult:
|
||||||
"""Compile a µFrame .uf source string into ASCII and Micron output.
|
"""Compile a µFrame .uf source string into ASCII and Micron output.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -102,6 +103,10 @@ def compile(source: str, width: int = 64) -> CompileResult:
|
|||||||
|
|
||||||
w = page.width
|
w = page.width
|
||||||
|
|
||||||
|
# 1b. Resolve theme (CLI flag overrides source directive)
|
||||||
|
theme_name = theme or page.theme_name or "default"
|
||||||
|
theme_def = get_theme(theme_name)
|
||||||
|
|
||||||
# 2. Measure
|
# 2. Measure
|
||||||
measure(page, w)
|
measure(page, w)
|
||||||
|
|
||||||
@@ -110,7 +115,7 @@ def compile(source: str, width: int = 64) -> CompileResult:
|
|||||||
|
|
||||||
# 4. Create grid and paint
|
# 4. Create grid and paint
|
||||||
grid = CharGrid(w, max(total_h, 1))
|
grid = CharGrid(w, max(total_h, 1))
|
||||||
paint(page, grid)
|
paint(page, grid, theme_def)
|
||||||
|
|
||||||
# 5. Merge borders
|
# 5. Merge borders
|
||||||
merge_borders(grid)
|
merge_borders(grid)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ def cmd_render(args: argparse.Namespace) -> int:
|
|||||||
source = Path(args.file).read_text(encoding="utf-8")
|
source = Path(args.file).read_text(encoding="utf-8")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = uframe.compile(source, width=args.width)
|
result = uframe.compile(source, width=args.width, theme=getattr(args, 'theme', ''))
|
||||||
except UFrameError as e:
|
except UFrameError as e:
|
||||||
print(f"Error: {e}", file=sys.stderr)
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
@@ -52,7 +52,7 @@ def cmd_compile(args: argparse.Namespace) -> int:
|
|||||||
source = source_path.read_text(encoding="utf-8")
|
source = source_path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = uframe.compile(source, width=args.width)
|
result = uframe.compile(source, width=args.width, theme=getattr(args, 'theme', ''))
|
||||||
except UFrameError as e:
|
except UFrameError as e:
|
||||||
print(f"Error: {e}", file=sys.stderr)
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
@@ -75,7 +75,7 @@ def cmd_check(args: argparse.Namespace) -> int:
|
|||||||
source = Path(args.file).read_text(encoding="utf-8")
|
source = Path(args.file).read_text(encoding="utf-8")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = uframe.compile(source, width=args.width)
|
result = uframe.compile(source, width=args.width, theme=getattr(args, 'theme', ''))
|
||||||
except UFrameError as e:
|
except UFrameError as e:
|
||||||
print(f"Error: {e}", file=sys.stderr)
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
@@ -97,7 +97,7 @@ def cmd_deploy(args: argparse.Namespace) -> int:
|
|||||||
dest_dir = Path(args.dest or os.path.expanduser("~/.nomadnetwork/storage/pages"))
|
dest_dir = Path(args.dest or os.path.expanduser("~/.nomadnetwork/storage/pages"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = uframe.compile(source, width=args.width)
|
result = uframe.compile(source, width=args.width, theme=getattr(args, 'theme', ''))
|
||||||
except UFrameError as e:
|
except UFrameError as e:
|
||||||
print(f"Error: {e}", file=sys.stderr)
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
@@ -130,12 +130,14 @@ def main() -> int:
|
|||||||
p_render.add_argument("--ascii", action="store_true", help="Output ASCII only")
|
p_render.add_argument("--ascii", action="store_true", help="Output ASCII only")
|
||||||
p_render.add_argument("--micron", action="store_true", help="Output Micron only")
|
p_render.add_argument("--micron", action="store_true", help="Output Micron only")
|
||||||
p_render.add_argument("--width", type=int, default=64, help="Page width (default: 64)")
|
p_render.add_argument("--width", type=int, default=64, help="Page width (default: 64)")
|
||||||
|
p_render.add_argument("--theme", default="", help="Theme name (default, nouveau, gothic, bamboo, circuit, brutalist)")
|
||||||
|
|
||||||
# compile
|
# compile
|
||||||
p_compile = sub.add_parser("compile", help="Compile to .mu file")
|
p_compile = sub.add_parser("compile", help="Compile to .mu file")
|
||||||
p_compile.add_argument("file", help="Path to .uf source file")
|
p_compile.add_argument("file", help="Path to .uf source file")
|
||||||
p_compile.add_argument("--out", help="Output file path (default: <name>.mu)")
|
p_compile.add_argument("--out", help="Output file path (default: <name>.mu)")
|
||||||
p_compile.add_argument("--width", type=int, default=64, help="Page width")
|
p_compile.add_argument("--width", type=int, default=64, help="Page width")
|
||||||
|
p_compile.add_argument("--theme", default="", help="Theme name")
|
||||||
|
|
||||||
# check
|
# check
|
||||||
p_check = sub.add_parser("check", help="Validate a .uf file")
|
p_check = sub.add_parser("check", help="Validate a .uf file")
|
||||||
@@ -147,6 +149,7 @@ def main() -> int:
|
|||||||
p_deploy.add_argument("file", help="Path to .uf source file")
|
p_deploy.add_argument("file", help="Path to .uf source file")
|
||||||
p_deploy.add_argument("--dest", help="Destination directory (default: ~/.nomadnetwork/storage/pages)")
|
p_deploy.add_argument("--dest", help="Destination directory (default: ~/.nomadnetwork/storage/pages)")
|
||||||
p_deploy.add_argument("--width", type=int, default=64, help="Page width")
|
p_deploy.add_argument("--width", type=int, default=64, help="Page width")
|
||||||
|
p_deploy.add_argument("--theme", default="", help="Theme name")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,9 @@ class CharGrid:
|
|||||||
def draw_border(self, x: int, y: int, w: int, h: int,
|
def draw_border(self, x: int, y: int, w: int, h: int,
|
||||||
weight: BorderWeight = BorderWeight.LIGHT,
|
weight: BorderWeight = BorderWeight.LIGHT,
|
||||||
title: str = "",
|
title: str = "",
|
||||||
title_style: CellStyle | None = None) -> None:
|
title_style: CellStyle | None = None,
|
||||||
|
border_chars: dict[str, str] | None = None,
|
||||||
|
title_caps: tuple[str, str] | None = None) -> None:
|
||||||
"""Draw a box border. Interior is not cleared.
|
"""Draw a box border. Interior is not cleared.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -122,7 +124,7 @@ class CharGrid:
|
|||||||
self._border_counter += 1
|
self._border_counter += 1
|
||||||
bid = self._border_counter
|
bid = self._border_counter
|
||||||
|
|
||||||
ch = BOX_CHARS[weight]
|
ch = border_chars or BOX_CHARS[weight]
|
||||||
border_style = CellStyle()
|
border_style = CellStyle()
|
||||||
|
|
||||||
# Corners
|
# Corners
|
||||||
@@ -143,12 +145,14 @@ class CharGrid:
|
|||||||
|
|
||||||
# Title in top border
|
# Title in top border
|
||||||
if title and w > 4:
|
if title and w > 4:
|
||||||
title_text = f" {title} "
|
lc = title_caps[0] if title_caps else " "
|
||||||
max_title = w - 4 # leave room for corners + padding
|
rc = title_caps[1] if title_caps else " "
|
||||||
|
title_text = f"{lc}{title}{rc}"
|
||||||
|
max_title = w - 4
|
||||||
if len(title_text) > max_title:
|
if len(title_text) > max_title:
|
||||||
title_text = title_text[:max_title]
|
title_text = title_text[:max_title]
|
||||||
|
|
||||||
start_x = x + 2
|
start_x = x + 1
|
||||||
ts = title_style or CellStyle(bold=True)
|
ts = title_style or CellStyle(bold=True)
|
||||||
self.put_text(start_x, y, title_text, style=ts)
|
self.put_text(start_x, y, title_text, style=ts)
|
||||||
|
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ class Page(IRNode):
|
|||||||
"""Root container. One per .uf file."""
|
"""Root container. One per .uf file."""
|
||||||
title: str = ""
|
title: str = ""
|
||||||
width: int = 64
|
width: int = 64
|
||||||
|
theme_name: str = ""
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ import re
|
|||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
from uframe.chars import (
|
from uframe.chars import (
|
||||||
BOX_CHARS, DIVIDER_CHARS, GAUGE_FILLED, GAUGE_EMPTY,
|
BOX_CHARS, DIVIDER_CHARS, sparkline_chars,
|
||||||
STATUS_CHARS, STATUS_COLORS, sparkline_chars,
|
|
||||||
)
|
)
|
||||||
from uframe.grid import CharGrid, CellStyle
|
from uframe.grid import CharGrid, CellStyle
|
||||||
from uframe.ir import (
|
from uframe.ir import (
|
||||||
@@ -22,6 +21,7 @@ from uframe.ir import (
|
|||||||
Form, Field, Password, Radio, Checkbox, FormButton,
|
Form, Field, Password, Radio, Checkbox, FormButton,
|
||||||
HeadingLevel, DividerStyle, ListStyle, Align, BorderWeight,
|
HeadingLevel, DividerStyle, ListStyle, Align, BorderWeight,
|
||||||
)
|
)
|
||||||
|
from uframe.themes import ThemeDef, THEME_DEFAULT
|
||||||
|
|
||||||
|
|
||||||
def _align_text(text: str, width: int, align: Align) -> str:
|
def _align_text(text: str, width: int, align: Align) -> str:
|
||||||
@@ -46,8 +46,9 @@ def _style_from_node(node: IRNode) -> CellStyle:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def paint(node: IRNode, grid: CharGrid) -> None:
|
def paint(node: IRNode, grid: CharGrid, theme: ThemeDef | None = None) -> None:
|
||||||
"""Recursively paint an IR node and its children into the grid."""
|
"""Recursively paint an IR node and its children into the grid."""
|
||||||
|
th = theme or THEME_DEFAULT
|
||||||
x, y, w = node.rect.x, node.rect.y, node.rect.w
|
x, y, w = node.rect.x, node.rect.y, node.rect.w
|
||||||
|
|
||||||
# Ensure grid is tall enough
|
# Ensure grid is tall enough
|
||||||
@@ -55,46 +56,48 @@ def paint(node: IRNode, grid: CharGrid) -> None:
|
|||||||
|
|
||||||
if isinstance(node, Page):
|
if isinstance(node, Page):
|
||||||
for child in node.children:
|
for child in node.children:
|
||||||
paint(child, grid)
|
paint(child, grid, th)
|
||||||
|
|
||||||
elif isinstance(node, Box):
|
elif isinstance(node, Box):
|
||||||
# Draw the border
|
# Draw the border with themed characters
|
||||||
title_style = CellStyle(bold=True, fg=node.style.fg)
|
title_style = CellStyle(bold=True, fg=node.style.fg or th.palette.accent)
|
||||||
grid.draw_border(x, y, w, node.rect.h,
|
grid.draw_border(x, y, w, node.rect.h,
|
||||||
weight=node.weight,
|
weight=node.weight,
|
||||||
title=node.title,
|
title=node.title,
|
||||||
title_style=title_style)
|
title_style=title_style,
|
||||||
|
border_chars=th.border_dict(node.weight.name.lower()),
|
||||||
|
title_caps=(th.title_caps.left, th.title_caps.right))
|
||||||
# Propagate box alignment/color to children that don't have their own
|
# Propagate box alignment/color to children that don't have their own
|
||||||
for child in node.children:
|
for child in node.children:
|
||||||
if node.style.align != Align.LEFT and child.style.align == Align.LEFT:
|
if node.style.align != Align.LEFT and child.style.align == Align.LEFT:
|
||||||
child.style.align = node.style.align
|
child.style.align = node.style.align
|
||||||
if node.style.fg and not child.style.fg:
|
if node.style.fg and not child.style.fg:
|
||||||
child.style.fg = node.style.fg
|
child.style.fg = node.style.fg
|
||||||
paint(child, grid)
|
paint(child, grid, th)
|
||||||
|
|
||||||
elif isinstance(node, Row):
|
elif isinstance(node, Row):
|
||||||
for child in node.children:
|
for child in node.children:
|
||||||
paint(child, grid)
|
paint(child, grid, th)
|
||||||
|
|
||||||
elif isinstance(node, Col):
|
elif isinstance(node, Col):
|
||||||
for child in node.children:
|
for child in node.children:
|
||||||
paint(child, grid)
|
paint(child, grid, th)
|
||||||
|
|
||||||
elif isinstance(node, Spacer):
|
elif isinstance(node, Spacer):
|
||||||
pass # Just empty space
|
pass # Just empty space
|
||||||
|
|
||||||
elif isinstance(node, Pad):
|
elif isinstance(node, Pad):
|
||||||
for child in node.children:
|
for child in node.children:
|
||||||
paint(child, grid)
|
paint(child, grid, th)
|
||||||
|
|
||||||
elif isinstance(node, Heading):
|
elif isinstance(node, Heading):
|
||||||
style = CellStyle(bold=True)
|
style = CellStyle(bold=True)
|
||||||
if node.level == HeadingLevel.H1:
|
if node.level == HeadingLevel.H1:
|
||||||
style.fg = "0f0" # green
|
style.fg = th.palette.accent
|
||||||
elif node.level == HeadingLevel.H2:
|
elif node.level == HeadingLevel.H2:
|
||||||
style.fg = "0cf" # cyan
|
style.fg = th.palette.accent2
|
||||||
elif node.level == HeadingLevel.H3:
|
elif node.level == HeadingLevel.H3:
|
||||||
style.fg = "88f" # light blue
|
style.fg = th.palette.accent3
|
||||||
|
|
||||||
# Underline-style heading
|
# Underline-style heading
|
||||||
grid.put_text(x, y, node.text[:w], style=style)
|
grid.put_text(x, y, node.text[:w], style=style)
|
||||||
@@ -143,26 +146,24 @@ def paint(node: IRNode, grid: CharGrid) -> None:
|
|||||||
|
|
||||||
elif isinstance(node, Divider):
|
elif isinstance(node, Divider):
|
||||||
ds = node.divider_style
|
ds = node.divider_style
|
||||||
char = DIVIDER_CHARS.get(ds.name.lower(), "─")
|
char = getattr(th.dividers, ds.name.lower(), th.dividers.light)
|
||||||
style = CellStyle(fg="555")
|
style = CellStyle(fg=th.palette.muted)
|
||||||
for col in range(x, x + w):
|
for col in range(x, x + w):
|
||||||
grid.put(col, y, char, style=style)
|
grid.put(col, y, char, style=style)
|
||||||
|
|
||||||
elif isinstance(node, Link):
|
elif isinstance(node, Link):
|
||||||
style = CellStyle(fg="0cf", underline=True)
|
style = CellStyle(fg=th.palette.info, underline=True)
|
||||||
# In ASCII mode, display as [text]. In Micron, the emitter wraps with link syntax.
|
|
||||||
# Write just the display text — the link metadata goes on cells for Micron emission.
|
|
||||||
grid.put_text(x, y, node.display[:w], style=style, link=node.dest)
|
grid.put_text(x, y, node.display[:w], style=style, link=node.dest)
|
||||||
|
|
||||||
elif isinstance(node, ListNode):
|
elif isinstance(node, ListNode):
|
||||||
for child in node.children:
|
for child in node.children:
|
||||||
paint(child, grid)
|
paint(child, grid, th)
|
||||||
|
|
||||||
elif isinstance(node, ListItem):
|
elif isinstance(node, ListItem):
|
||||||
style = _style_from_node(node)
|
style = _style_from_node(node)
|
||||||
# Bullet to the left of the content (safe: put_text clips to bounds)
|
# Bullet to the left of the content (safe: put_text clips to bounds)
|
||||||
bullet_x = max(0, x - 2)
|
bullet_x = max(0, x - 2)
|
||||||
grid.put_text(bullet_x, y, "• ", style=CellStyle(fg="888"))
|
grid.put_text(bullet_x, y, f"{th.ornaments.bullet} ", style=CellStyle(fg=th.palette.label))
|
||||||
# Wrap content
|
# Wrap content
|
||||||
wrapped = textwrap.wrap(node.content, width=w) if node.content else [""]
|
wrapped = textwrap.wrap(node.content, width=w) if node.content else [""]
|
||||||
for i, line in enumerate(wrapped):
|
for i, line in enumerate(wrapped):
|
||||||
@@ -183,17 +184,17 @@ def paint(node: IRNode, grid: CharGrid) -> None:
|
|||||||
filled = int(bar_w * pct)
|
filled = int(bar_w * pct)
|
||||||
|
|
||||||
# Determine color based on thresholds
|
# Determine color based on thresholds
|
||||||
fg = "0f0" # green
|
fg = th.palette.success
|
||||||
if node.crit is not None and node.value >= node.crit:
|
if node.crit is not None and node.value >= node.crit:
|
||||||
fg = "f00" # red
|
fg = th.palette.danger
|
||||||
elif node.warn is not None and node.value >= node.warn:
|
elif node.warn is not None and node.value >= node.warn:
|
||||||
fg = "ff0" # yellow
|
fg = th.palette.warning
|
||||||
|
|
||||||
for i in range(bar_w):
|
for i in range(bar_w):
|
||||||
if i < filled:
|
if i < filled:
|
||||||
grid.put(bar_x + i, y, GAUGE_FILLED, style=CellStyle(fg=fg))
|
grid.put(bar_x + i, y, th.gauge.filled, style=CellStyle(fg=fg))
|
||||||
else:
|
else:
|
||||||
grid.put(bar_x + i, y, GAUGE_EMPTY, style=CellStyle(fg="555"))
|
grid.put(bar_x + i, y, th.gauge.empty, style=CellStyle(fg=th.palette.muted))
|
||||||
|
|
||||||
# Percentage
|
# Percentage
|
||||||
pct_text = f" {int(pct * 100)}%"
|
pct_text = f" {int(pct * 100)}%"
|
||||||
@@ -206,13 +207,16 @@ def paint(node: IRNode, grid: CharGrid) -> None:
|
|||||||
|
|
||||||
spark_x = x + len(label_text)
|
spark_x = x + len(label_text)
|
||||||
chars = sparkline_chars(node.values, node.spark_width)
|
chars = sparkline_chars(node.values, node.spark_width)
|
||||||
spark_style = CellStyle(fg="0cf")
|
spark_style = CellStyle(fg=th.palette.info)
|
||||||
for i, ch in enumerate(chars):
|
for i, ch in enumerate(chars):
|
||||||
grid.put(spark_x + i, y, ch, style=spark_style)
|
grid.put(spark_x + i, y, ch, style=spark_style)
|
||||||
|
|
||||||
elif isinstance(node, Status):
|
elif isinstance(node, Status):
|
||||||
char = STATUS_CHARS.get(node.state, "◌")
|
char = getattr(th.indicators, node.state, th.indicators.unknown)
|
||||||
color = STATUS_COLORS.get(node.state, "888")
|
color_map = {"online": th.palette.success, "offline": th.palette.danger,
|
||||||
|
"degraded": th.palette.warning, "unknown": th.palette.label,
|
||||||
|
"alert": th.palette.danger}
|
||||||
|
color = color_map.get(node.state, th.palette.label)
|
||||||
grid.put(x, y, char, style=CellStyle(fg=color))
|
grid.put(x, y, char, style=CellStyle(fg=color))
|
||||||
grid.put_text(x + 2, y, node.label)
|
grid.put_text(x + 2, y, node.label)
|
||||||
|
|
||||||
@@ -221,62 +225,65 @@ def paint(node: IRNode, grid: CharGrid) -> None:
|
|||||||
|
|
||||||
elif isinstance(node, Form):
|
elif isinstance(node, Form):
|
||||||
for child in node.children:
|
for child in node.children:
|
||||||
paint(child, grid)
|
paint(child, grid, th)
|
||||||
|
|
||||||
elif isinstance(node, Field):
|
elif isinstance(node, Field):
|
||||||
label_style = CellStyle(fg="888")
|
label_style = CellStyle(fg=th.palette.label)
|
||||||
field_style = CellStyle(fg="0cf")
|
field_style = CellStyle(fg=th.palette.form)
|
||||||
label_text = f"{node.field_name}: "
|
label_text = f"{node.field_name}: "
|
||||||
grid.put_text(x, y, label_text, style=label_style)
|
grid.put_text(x, y, label_text, style=label_style)
|
||||||
# Draw [ placeholder_______ ]
|
fl = th.form.field_l
|
||||||
|
fr = th.form.field_r
|
||||||
fx = x + len(label_text)
|
fx = x + len(label_text)
|
||||||
fw = min(node.field_width, w - len(label_text) - 2)
|
fw = min(node.field_width, w - len(label_text) - len(fl) - len(fr))
|
||||||
grid.put(fx, y, "[", style=field_style)
|
grid.put_text(fx, y, fl, style=field_style)
|
||||||
placeholder = node.placeholder or node.field_name
|
placeholder = node.placeholder or node.field_name
|
||||||
inner = f" {placeholder}".ljust(fw - 1)[:fw - 1]
|
inner = placeholder.ljust(fw)[:fw]
|
||||||
grid.put_text(fx + 1, y, inner, style=CellStyle(fg="555"))
|
grid.put_text(fx + len(fl), y, inner, style=CellStyle(fg=th.palette.muted))
|
||||||
grid.put(fx + fw, y, "]", style=field_style)
|
grid.put_text(fx + len(fl) + fw, y, fr, style=field_style)
|
||||||
|
|
||||||
elif isinstance(node, Password):
|
elif isinstance(node, Password):
|
||||||
label_style = CellStyle(fg="888")
|
label_style = CellStyle(fg=th.palette.label)
|
||||||
field_style = CellStyle(fg="0cf")
|
field_style = CellStyle(fg=th.palette.form)
|
||||||
label_text = f"{node.field_name}: "
|
label_text = f"{node.field_name}: "
|
||||||
grid.put_text(x, y, label_text, style=label_style)
|
grid.put_text(x, y, label_text, style=label_style)
|
||||||
|
fl = th.form.field_l
|
||||||
|
fr = th.form.field_r
|
||||||
fx = x + len(label_text)
|
fx = x + len(label_text)
|
||||||
fw = min(node.field_width, w - len(label_text) - 2)
|
fw = min(node.field_width, w - len(label_text) - len(fl) - len(fr))
|
||||||
grid.put(fx, y, "[", style=field_style)
|
grid.put_text(fx, y, fl, style=field_style)
|
||||||
inner = " " + "•" * (fw - 2)
|
inner = "•" * fw
|
||||||
grid.put_text(fx + 1, y, inner[:fw - 1], style=CellStyle(fg="555"))
|
grid.put_text(fx + len(fl), y, inner[:fw], style=CellStyle(fg=th.palette.muted))
|
||||||
grid.put(fx + fw, y, "]", style=field_style)
|
grid.put_text(fx + len(fl) + fw, y, fr, style=field_style)
|
||||||
|
|
||||||
elif isinstance(node, Radio):
|
elif isinstance(node, Radio):
|
||||||
label_style = CellStyle(fg="888")
|
label_style = CellStyle(fg=th.palette.label)
|
||||||
label_text = f"{node.group}: "
|
label_text = f"{node.group}: "
|
||||||
grid.put_text(x, y, label_text, style=label_style)
|
grid.put_text(x, y, label_text, style=label_style)
|
||||||
rx = x + len(label_text)
|
rx = x + len(label_text)
|
||||||
for i, opt in enumerate(node.options):
|
for i, opt in enumerate(node.options):
|
||||||
dot = "(•)" if i == 0 else "( )"
|
dot = th.form.radio_on if i == 0 else th.form.radio_off
|
||||||
opt_style = CellStyle(fg="0cf" if i == 0 else "888")
|
opt_style = CellStyle(fg=th.palette.form if i == 0 else th.palette.label)
|
||||||
grid.put_text(rx, y, dot, style=opt_style)
|
grid.put_text(rx, y, dot, style=opt_style)
|
||||||
rx += 4
|
rx += len(dot) + 1
|
||||||
grid.put_text(rx, y, opt, style=CellStyle())
|
grid.put_text(rx, y, opt, style=CellStyle())
|
||||||
rx += len(opt) + 2
|
rx += len(opt) + 2
|
||||||
|
|
||||||
elif isinstance(node, Checkbox):
|
elif isinstance(node, Checkbox):
|
||||||
check_style = CellStyle(fg="0cf")
|
check_style = CellStyle(fg=th.palette.form)
|
||||||
box_char = "[✓]" if node.checked else "[ ]"
|
box_char = th.form.check_on if node.checked else th.form.check_off
|
||||||
grid.put_text(x, y, box_char, style=check_style)
|
grid.put_text(x, y, box_char, style=check_style)
|
||||||
grid.put_text(x + 4, y, node.checkbox_label)
|
grid.put_text(x + len(box_char) + 1, y, node.checkbox_label)
|
||||||
|
|
||||||
elif isinstance(node, FormButton):
|
elif isinstance(node, FormButton):
|
||||||
btn_style = CellStyle(bold=True, fg="0f0")
|
btn_style = CellStyle(bold=True, fg=th.palette.button)
|
||||||
btn_text = f"[ {node.button_label} ]"
|
btn_text = f"[ {node.button_label} ]"
|
||||||
grid.put_text(x, y, btn_text, style=btn_style, link=node.dest)
|
grid.put_text(x, y, btn_text, style=btn_style, link=node.dest)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Generic: paint children
|
# Generic: paint children
|
||||||
for child in node.children:
|
for child in node.children:
|
||||||
paint(child, grid)
|
paint(child, grid, th)
|
||||||
|
|
||||||
|
|
||||||
def _paint_table(node: Table, grid: CharGrid, x: int, y: int, w: int) -> None:
|
def _paint_table(node: Table, grid: CharGrid, x: int, y: int, w: int) -> None:
|
||||||
|
|||||||
@@ -426,6 +426,11 @@ def _parse_line(keyword: str, args: list[str], line_num: int) -> IRNode:
|
|||||||
content = " ".join([keyword] + args)
|
content = " ".join([keyword] + args)
|
||||||
return Text(content=content, source_line=line_num)
|
return Text(content=content, source_line=line_num)
|
||||||
|
|
||||||
|
elif keyword == "theme":
|
||||||
|
# theme "name" — sets the page theme (handled as _StyleDirective on Page)
|
||||||
|
theme_name = args[0] if args else "default"
|
||||||
|
return _ThemeDirective(theme_name, line_num)
|
||||||
|
|
||||||
elif keyword == "component":
|
elif keyword == "component":
|
||||||
# component name(arg1, arg2)
|
# component name(arg1, arg2)
|
||||||
raw = " ".join(args)
|
raw = " ".join(args)
|
||||||
@@ -450,6 +455,13 @@ def _parse_line(keyword: str, args: list[str], line_num: int) -> IRNode:
|
|||||||
return ComponentUse(comp_name=keyword, args=args, source_line=line_num)
|
return ComponentUse(comp_name=keyword, args=args, source_line=line_num)
|
||||||
|
|
||||||
|
|
||||||
|
class _ThemeDirective(IRNode):
|
||||||
|
"""Temporary node — sets theme_name on the Page during tree building."""
|
||||||
|
def __init__(self, theme_name: str, line_num: int):
|
||||||
|
super().__init__(source_line=line_num)
|
||||||
|
self.theme_name = theme_name
|
||||||
|
|
||||||
|
|
||||||
class _UseDirective(IRNode):
|
class _UseDirective(IRNode):
|
||||||
"""Temporary node — triggers library loading during tree building."""
|
"""Temporary node — triggers library loading during tree building."""
|
||||||
def __init__(self, lib_path: str, line_num: int):
|
def __init__(self, lib_path: str, line_num: int):
|
||||||
@@ -756,12 +768,17 @@ def parse(source: str, components: dict[str, ComponentDef] | None = None) -> Pag
|
|||||||
stack.pop()
|
stack.pop()
|
||||||
|
|
||||||
if isinstance(node, _StyleDirective):
|
if isinstance(node, _StyleDirective):
|
||||||
# Apply style directive to the current top of stack (parent)
|
|
||||||
if stack:
|
if stack:
|
||||||
parent = stack[-1][1]
|
parent = stack[-1][1]
|
||||||
setattr(parent.style, node.attr, node.value)
|
setattr(parent.style, node.attr, node.value)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if isinstance(node, _ThemeDirective):
|
||||||
|
# Set theme on the root Page
|
||||||
|
if root and isinstance(root, Page):
|
||||||
|
root.theme_name = node.theme_name
|
||||||
|
continue
|
||||||
|
|
||||||
# Table children: columns and rows are absorbed by the Table node
|
# Table children: columns and rows are absorbed by the Table node
|
||||||
if isinstance(node, _TableColumns):
|
if isinstance(node, _TableColumns):
|
||||||
if stack and isinstance(stack[-1][1], Table):
|
if stack and isinstance(stack[-1][1], Table):
|
||||||
|
|||||||
241
backend/uframe/themes.py
Normal file
241
backend/uframe/themes.py
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
"""µFrame Theme System — decorative styles for rich terminal UIs.
|
||||||
|
|
||||||
|
A theme maps abstract UI elements to concrete character sets and color
|
||||||
|
palettes. The same .uf source renders with different visual character
|
||||||
|
when a different theme is applied.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BorderChars:
|
||||||
|
tl: str = "┌"; t: str = "─"; tr: str = "┐"
|
||||||
|
l: str = "│"; r: str = "│"
|
||||||
|
bl: str = "└"; b: str = "─"; br: str = "┘"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Indicators:
|
||||||
|
online: str = "●"
|
||||||
|
offline: str = "○"
|
||||||
|
degraded: str = "◐"
|
||||||
|
unknown: str = "◌"
|
||||||
|
alert: str = "⚠"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GaugeChars:
|
||||||
|
filled: str = "█"
|
||||||
|
empty: str = "░"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FormChars:
|
||||||
|
field_l: str = "[ "
|
||||||
|
field_r: str = " ]"
|
||||||
|
radio_on: str = "(•)"
|
||||||
|
radio_off: str = "( )"
|
||||||
|
check_on: str = "[✓]"
|
||||||
|
check_off: str = "[ ]"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Ornaments:
|
||||||
|
bullet: str = "•"
|
||||||
|
header: str = ""
|
||||||
|
separator: str = ""
|
||||||
|
footer: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TitleCaps:
|
||||||
|
left: str = "─ "
|
||||||
|
right: str = " ─"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Palette:
|
||||||
|
accent: str = "0f0" # headings, primary highlights
|
||||||
|
accent2: str = "0cf" # secondary (H2, links)
|
||||||
|
accent3: str = "88f" # tertiary (H3)
|
||||||
|
muted: str = "555" # dividers, empty gauge
|
||||||
|
border: str = "" # border color (empty = no color)
|
||||||
|
success: str = "0f0" # online, gauge ok
|
||||||
|
warning: str = "ff0" # degraded, gauge warn
|
||||||
|
danger: str = "f00" # offline, gauge crit
|
||||||
|
info: str = "0cf" # links, sparklines
|
||||||
|
form: str = "0cf" # form element accents
|
||||||
|
label: str = "888" # labels, field names
|
||||||
|
button: str = "0f0" # form buttons
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DividerChars:
|
||||||
|
light: str = "─"
|
||||||
|
heavy: str = "━"
|
||||||
|
double: str = "═"
|
||||||
|
dash: str = "╌"
|
||||||
|
dot: str = "┄"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ThemeDef:
|
||||||
|
name: str = "default"
|
||||||
|
description: str = "Clean engineering — standard box-drawing"
|
||||||
|
|
||||||
|
borders_light: BorderChars = field(default_factory=BorderChars)
|
||||||
|
borders_heavy: BorderChars = field(default_factory=lambda: BorderChars(
|
||||||
|
tl="┏", t="━", tr="┓", l="┃", r="┃", bl="┗", b="━", br="┛"))
|
||||||
|
borders_double: BorderChars = field(default_factory=lambda: BorderChars(
|
||||||
|
tl="╔", t="═", tr="╗", l="║", r="║", bl="╚", b="═", br="╝"))
|
||||||
|
borders_rounded: BorderChars = field(default_factory=lambda: BorderChars(
|
||||||
|
tl="╭", t="─", tr="╮", l="│", r="│", bl="╰", b="─", br="╯"))
|
||||||
|
|
||||||
|
dividers: DividerChars = field(default_factory=DividerChars)
|
||||||
|
title_caps: TitleCaps = field(default_factory=TitleCaps)
|
||||||
|
indicators: Indicators = field(default_factory=Indicators)
|
||||||
|
gauge: GaugeChars = field(default_factory=GaugeChars)
|
||||||
|
form: FormChars = field(default_factory=FormChars)
|
||||||
|
ornaments: Ornaments = field(default_factory=Ornaments)
|
||||||
|
palette: Palette = field(default_factory=Palette)
|
||||||
|
|
||||||
|
def border_chars(self, weight_name: str) -> BorderChars:
|
||||||
|
return {
|
||||||
|
"light": self.borders_light,
|
||||||
|
"heavy": self.borders_heavy,
|
||||||
|
"double": self.borders_double,
|
||||||
|
"rounded": self.borders_rounded,
|
||||||
|
}.get(weight_name, self.borders_light)
|
||||||
|
|
||||||
|
def border_dict(self, weight_name: str) -> dict[str, str]:
|
||||||
|
"""Return BOX_CHARS-compatible dict for a border weight."""
|
||||||
|
bc = self.border_chars(weight_name)
|
||||||
|
return {
|
||||||
|
"tl": bc.tl, "tr": bc.tr, "bl": bc.bl, "br": bc.br,
|
||||||
|
"h": bc.t, "v": bc.l,
|
||||||
|
"t_down": bc.t, "t_up": bc.b, "t_right": bc.l, "t_left": bc.r,
|
||||||
|
"cross": bc.t,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Built-in themes
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
THEME_DEFAULT = ThemeDef()
|
||||||
|
|
||||||
|
THEME_NOUVEAU = ThemeDef(
|
||||||
|
name="nouveau",
|
||||||
|
description="Art Nouveau — organic flowing ornament",
|
||||||
|
borders_heavy=BorderChars(tl="☙", t="━", tr="❧", l="┃", r="┃", bl="☙", b="━", br="❧"),
|
||||||
|
borders_light=BorderChars(tl="╭", t="┈", tr="╮", l="┊", r="┊", bl="╰", b="┈", br="╯"),
|
||||||
|
borders_double=BorderChars(tl="☙", t="━", tr="❧", l="┃", r="┃", bl="☙", b="━", br="❧"),
|
||||||
|
borders_rounded=BorderChars(tl="╭", t="┈", tr="╮", l="┊", r="┊", bl="╰", b="┈", br="╯"),
|
||||||
|
dividers=DividerChars(light="┈", heavy="━", double="━", dash="┈", dot="┈"),
|
||||||
|
title_caps=TitleCaps(left="✾─── ", right=" ───✾"),
|
||||||
|
indicators=Indicators(online="❀", offline="✿", degraded="⚘", unknown="✿", alert="❋"),
|
||||||
|
gauge=GaugeChars(filled="▐", empty="░"),
|
||||||
|
form=FormChars(field_l="❴ ", field_r=" ❵", radio_on="❀", radio_off="✿",
|
||||||
|
check_on="❀", check_off="✿"),
|
||||||
|
ornaments=Ornaments(bullet="❀", header="─✾──────✾─", separator="☙━━━━━━━━━━━━━❧"),
|
||||||
|
palette=Palette(accent="da5", accent2="8b5", accent3="886", muted="886",
|
||||||
|
border="a85", success="6b4", warning="da5", danger="a33",
|
||||||
|
info="68a", form="da5", label="886", button="6b4"),
|
||||||
|
)
|
||||||
|
|
||||||
|
THEME_GOTHIC = ThemeDef(
|
||||||
|
name="gothic",
|
||||||
|
description="Gothic — heavy blackletter, monumental",
|
||||||
|
borders_heavy=BorderChars(tl="╬", t="═", tr="╬", l="║", r="║", bl="╬", b="═", br="╬"),
|
||||||
|
borders_light=BorderChars(tl="╔", t="═", tr="╗", l="║", r="║", bl="╚", b="═", br="╝"),
|
||||||
|
borders_double=BorderChars(tl="╬", t="═", tr="╬", l="║", r="║", bl="╬", b="═", br="╬"),
|
||||||
|
borders_rounded=BorderChars(tl="╔", t="═", tr="╗", l="║", r="║", bl="╚", b="═", br="╝"),
|
||||||
|
dividers=DividerChars(light="═", heavy="═", double="═", dash="═", dot="═"),
|
||||||
|
title_caps=TitleCaps(left="═══╡ ", right=" ╞═══"),
|
||||||
|
indicators=Indicators(online="⚑", offline="⚐", degraded="⚑", unknown="⚐", alert="⚔"),
|
||||||
|
gauge=GaugeChars(filled="▓", empty="░"),
|
||||||
|
form=FormChars(field_l="║ ", field_r=" ║", radio_on="⚑", radio_off="⚐",
|
||||||
|
check_on="⚑", check_off="⚐"),
|
||||||
|
ornaments=Ornaments(bullet="▪", header="═══╡══════╞═══"),
|
||||||
|
palette=Palette(accent="cc8", accent2="a66", accent3="888", muted="666",
|
||||||
|
border="888", success="8a8", warning="cc8", danger="a44",
|
||||||
|
info="8ac", form="cc8", label="888", button="cc8"),
|
||||||
|
)
|
||||||
|
|
||||||
|
THEME_BAMBOO = ThemeDef(
|
||||||
|
name="bamboo",
|
||||||
|
description="Bamboo — East Asian minimalism, light brush strokes",
|
||||||
|
borders_heavy=BorderChars(tl="〔", t=" ", tr="〕", l=" ", r=" ", bl=" ", b=" ", br=" "),
|
||||||
|
borders_light=BorderChars(tl="┌", t="╌", tr="┐", l="╎", r="╎", bl="└", b="╌", br="┘"),
|
||||||
|
borders_double=BorderChars(tl="〔", t=" ", tr="〕", l=" ", r=" ", bl=" ", b=" ", br=" "),
|
||||||
|
borders_rounded=BorderChars(tl="┌", t="╌", tr="┐", l="╎", r="╎", bl="└", b="╌", br="┘"),
|
||||||
|
dividers=DividerChars(light="┄", heavy="┄", double="┄", dash="┄", dot="┄"),
|
||||||
|
title_caps=TitleCaps(left="┄┄┄ ", right=" ┄┄┄"),
|
||||||
|
indicators=Indicators(online="◉", offline="◦", degraded="◎", unknown="◦", alert="◈"),
|
||||||
|
gauge=GaugeChars(filled="▏", empty=" "),
|
||||||
|
form=FormChars(field_l="〈 ", field_r=" 〉", radio_on="◉", radio_off="◦",
|
||||||
|
check_on="◉", check_off="◦"),
|
||||||
|
ornaments=Ornaments(bullet="‣"),
|
||||||
|
palette=Palette(accent="bca", accent2="ab9", accent3="998", muted="998",
|
||||||
|
border="776", success="8b8", warning="cc9", danger="b77",
|
||||||
|
info="9ab", form="bca", label="998", button="8b8"),
|
||||||
|
)
|
||||||
|
|
||||||
|
THEME_CIRCUIT = ThemeDef(
|
||||||
|
name="circuit",
|
||||||
|
description="Circuit — digital, technical, neon",
|
||||||
|
borders_heavy=BorderChars(tl="╒", t="═", tr="╕", l="│", r="│", bl="╘", b="═", br="╛"),
|
||||||
|
borders_light=BorderChars(tl="┌", t="─", tr="┐", l="│", r="│", bl="└", b="─", br="┘"),
|
||||||
|
borders_double=BorderChars(tl="╒", t="═", tr="╕", l="│", r="│", bl="╘", b="═", br="╛"),
|
||||||
|
borders_rounded=BorderChars(tl="╒", t="═", tr="╕", l="│", r="│", bl="╘", b="═", br="╛"),
|
||||||
|
dividers=DividerChars(light="─", heavy="═", double="═", dash="╌", dot="┄"),
|
||||||
|
title_caps=TitleCaps(left="══[ ", right=" ]═══"),
|
||||||
|
indicators=Indicators(online="◈", offline="◇", degraded="◈", unknown="◇", alert="⚡"),
|
||||||
|
gauge=GaugeChars(filled="▰", empty="▱"),
|
||||||
|
form=FormChars(field_l=">_ [ ", field_r=" ]", radio_on="[▰]", radio_off="[▱]",
|
||||||
|
check_on="[▰]", check_off="[▱]"),
|
||||||
|
ornaments=Ornaments(bullet="▸"),
|
||||||
|
palette=Palette(accent="0ff", accent2="f0f", accent3="0af", muted="555",
|
||||||
|
border="0aa", success="0f0", warning="ff0", danger="f00",
|
||||||
|
info="0ff", form="0ff", label="0aa", button="0f0"),
|
||||||
|
)
|
||||||
|
|
||||||
|
THEME_BRUTALIST = ThemeDef(
|
||||||
|
name="brutalist",
|
||||||
|
description="Brutalist — raw blocks, monochrome, anti-decorative",
|
||||||
|
borders_heavy=BorderChars(tl="█", t="█", tr="█", l="█", r="█", bl="█", b="█", br="█"),
|
||||||
|
borders_light=BorderChars(tl="▛", t="▀", tr="▜", l="▌", r="▐", bl="▙", b="▄", br="▟"),
|
||||||
|
borders_double=BorderChars(tl="█", t="█", tr="█", l="█", r="█", bl="█", b="█", br="█"),
|
||||||
|
borders_rounded=BorderChars(tl="▛", t="▀", tr="▜", l="▌", r="▐", bl="▙", b="▄", br="▟"),
|
||||||
|
dividers=DividerChars(light="▔", heavy="█", double="█", dash="▔", dot="▔"),
|
||||||
|
title_caps=TitleCaps(left="▌ ", right=" ▐"),
|
||||||
|
indicators=Indicators(online="■", offline="□", degraded="■", unknown="□", alert="!"),
|
||||||
|
gauge=GaugeChars(filled="█", empty=" "),
|
||||||
|
form=FormChars(field_l="[", field_r="]", radio_on="■", radio_off="□",
|
||||||
|
check_on="■", check_off="□"),
|
||||||
|
ornaments=Ornaments(bullet="▪"),
|
||||||
|
palette=Palette(accent="fff", accent2="fff", accent3="ccc", muted="888",
|
||||||
|
border="fff", success="fff", warning="fff", danger="fff",
|
||||||
|
info="fff", form="fff", label="aaa", button="fff"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Theme registry
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
BUILTIN_THEMES: dict[str, ThemeDef] = {
|
||||||
|
"default": THEME_DEFAULT,
|
||||||
|
"nouveau": THEME_NOUVEAU,
|
||||||
|
"gothic": THEME_GOTHIC,
|
||||||
|
"bamboo": THEME_BAMBOO,
|
||||||
|
"circuit": THEME_CIRCUIT,
|
||||||
|
"brutalist": THEME_BRUTALIST,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_theme(name: str) -> ThemeDef:
|
||||||
|
"""Get a built-in theme by name. Returns default if not found."""
|
||||||
|
return BUILTIN_THEMES.get(name.lower(), THEME_DEFAULT)
|
||||||
@@ -278,6 +278,35 @@ export const EXAMPLES: Example[] = [
|
|||||||
peer_status "Node Gamma" degraded
|
peer_status "Node Gamma" degraded
|
||||||
|
|
||||||
divider heavy
|
divider heavy
|
||||||
|
link "Home" "/page/index.mu"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Themed Page",
|
||||||
|
description: "Same layout with different visual themes (try: nouveau, gothic, bamboo, circuit, brutalist)",
|
||||||
|
source: `page "Node Status" 50
|
||||||
|
theme nouveau
|
||||||
|
|
||||||
|
box heavy "Relay Alpha-7"
|
||||||
|
align center
|
||||||
|
text "Reticulum Network Node"
|
||||||
|
|
||||||
|
spacer
|
||||||
|
|
||||||
|
heading 1 "Resources"
|
||||||
|
|
||||||
|
gauge "CPU" 62 100 24 warn=75 crit=90
|
||||||
|
gauge "MEM" 84 100 24 warn=80 crit=95
|
||||||
|
|
||||||
|
spacer
|
||||||
|
|
||||||
|
heading 2 "Peers"
|
||||||
|
|
||||||
|
status "East Relay" online
|
||||||
|
status "South Bridge" online
|
||||||
|
status "Node Gamma" degraded
|
||||||
|
|
||||||
|
divider heavy
|
||||||
|
|
||||||
link "Home" "/page/index.mu"`,
|
link "Home" "/page/index.mu"`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -155,6 +155,44 @@ const COMMANDS: CmdEntry[] = [
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Themes — type /theme to filter all 6
|
||||||
|
{
|
||||||
|
label: "theme_default",
|
||||||
|
detail: "clean box-drawing ┌─┐●█░",
|
||||||
|
section: "Theme",
|
||||||
|
apply: insert("theme default"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "theme_nouveau",
|
||||||
|
detail: "flowing ornament ☙❧❀▐",
|
||||||
|
section: "Theme",
|
||||||
|
apply: insert("theme nouveau"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "theme_gothic",
|
||||||
|
detail: "blackletter ╬═║⚑▓",
|
||||||
|
section: "Theme",
|
||||||
|
apply: insert("theme gothic"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "theme_bamboo",
|
||||||
|
detail: "minimal brush 〔〕◉┄",
|
||||||
|
section: "Theme",
|
||||||
|
apply: insert("theme bamboo"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "theme_circuit",
|
||||||
|
detail: "digital neon ╒▰◈⚡",
|
||||||
|
section: "Theme",
|
||||||
|
apply: insert("theme circuit"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "theme_brutalist",
|
||||||
|
detail: "raw blocks █▌■□",
|
||||||
|
section: "Theme",
|
||||||
|
apply: insert("theme brutalist"),
|
||||||
|
},
|
||||||
|
|
||||||
// Templates
|
// Templates
|
||||||
{
|
{
|
||||||
label: "dashboard",
|
label: "dashboard",
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ export default function EditorView() {
|
|||||||
autocompletion({
|
autocompletion({
|
||||||
override: [uframeCommandSource],
|
override: [uframeCommandSource],
|
||||||
icons: false,
|
icons: false,
|
||||||
|
activateOnTyping: true,
|
||||||
|
maxOptions: 50,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
[],
|
[],
|
||||||
|
|||||||
Reference in New Issue
Block a user