Hi everyone!
For a long time I've been wanting to have a web development programming language that combined everything I enjoy about React + Styled Components and Python. Naturally I was drawn to Nim because of its syntax and extensibility. For a while now, on and off, I've been working on this project, and now I think I've gotten it to a point where I'm happy sharing it. Since Nim already has Karax I opted for signals instead of a virtual DOM. Feedback and contributions are more than welcome. The source can be found at https://github.com/jmsapps/ntml, and I have some runnable examples there if you'd like to play around with it!
Here's a basic example of a nav bar:
when defined(js):
import "ntml"
import "styles"
import "components/slider/index"
import "hooks/useTheme/index"
import "global/theme/index"
proc NavBar*(): Node =
let theme = useTheme()
let router = router()
let colorMode = signal(theme.get() == "dark")
let logo = derived(colorMode, proc (x: bool): string = (if x: "light" else: "dark"))
NavBarContainer:
BrandLogo(
src="/assets/logo_" & logo & ".svg",
alt="JMS APPS",
onClick = proc (e: Event) = navigate("/")
)
NavLinks:
if "ntml" in router.location:
NavButton(`type`="button", onClick = proc (e: Event) = navigate("/")):
"Home"
else:
NavButton(`type`="button", onClick = proc (e: Event) = navigate("/ntml")):
"NTML Docs"
NavToggleWrap:
Slider(
isToggled = colorMode,
labelText = "Toggle color mode",
leftSlot = block:
span: "☀️",
rightSlot = block:
span: "🌙",
onToggle = proc (next: bool) =
if next:
setStyledTheme(DarkTheme)
theme.set("dark")
else:
setStyledTheme(LightTheme)
theme.set("light")
)
styled macros look like this:
styled NavBarContainer = nav:
"""
position: sticky;
top: 0;
z-index: 20;
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem clamp(1.25rem, 4vw, 4rem);
background: var(--surface-muted, rgba(255, 255, 255, 0.9));
backdrop-filter: blur(18px);
border-bottom: 1px solid var(--border);
box-shadow: var(--shadow);
"""
Vars are supported (shown above in the styled example) which can be set globally using the theme macro:
theme LightTheme:
primary = "#3b5bff"
secondary = "#ff8a4c"
tertiary = "#15c9b9"
accent = "#8b5cf6"
background = "linear-gradient(140deg, #f6f9ff 0%, #edf1ff 45%, #fbfbfe 100%)"
surface = "#ffffff"
"surface-muted" = "rgba(255, 255, 255, 0.88)"
foreground = "#0f172a"
muted = "#4c5773"
white = "#ffffff"
black = "#050912"
grey = "#94a3b8"
border = "rgba(15, 23, 42, 0.08)"
highlight = "rgba(59, 91, 255, 0.08)"
glow = "0 20px 45px rgba(59, 91, 255, 0.25)"
shadow = "0 30px 70px rgba(15, 23, 42, 0.12)"
"shadow-strong" = "0 40px 80px rgba(15, 23, 42, 0.2)"
Vars can also be passed as props to a component with signals using styleVars. For example:
SliderFill(
class = "slider-fill",
styleVars = styleVars(
"--slider-thumb-color" = thumbColor,
"--slider-thumb-color-active" = thumbActive
)
)
Here's an example of the entry point for an application, where you can see that basic routing is also supported:
when defined(js):
import "ntml"
import "containers/home/index"
import "containers/ntml/index"
import "components/notFound/index"
import "hooks/useTheme/index"
import "global/theme/index"
proc App(): Node =
let router: Router = router()
let location: Signal[string] = router.location
let theme = useTheme()
setStyledTheme(if theme.get() == "dark": DarkTheme else: LightTheme)
Routes(location):
Route(path="/", component=Home)
Route(path="/ntml", component=NtmlDocsOverview):
Route(path="/ntml/overview", component=NtmlDocsOverview)
Route(path="/ntml/getting-started", component=NtmlDocsGettingStarted)
Route(path="/ntml/elements", component=NtmlDocsElements)
Route(path="/ntml/signals", component=NtmlDocsSignals)
Route(path="/ntml/overloads", component=NtmlDocsOverloads)
Route(path="/ntml/effects", component=NtmlDocsEffects)
Route(path="/ntml/control-flow", component=NtmlDocsControlFlow)
Route(path="/ntml/routing", component=NtmlDocsRouting)
Route(path="/ntml/styling", component=NtmlDocsStyling)
Route(path="/ntml/forms", component=NtmlDocsForms)
Route(path="*", component=NotFound)
try:
render(App())
except Exception as e:
echo e.repr
Nice! How is support looking for accessibility? Can I set arbitrary attributes and implement i.e. a fully blown combobox pattern or treeview with all the necessary aria attributes that need to be dynamically updated, for reference: https://www.w3.org/WAI/ARIA/apg/patterns/combobox/
How is your state tracking working, aka when is a component re rendered? What would be performance pitfalls one could fall into?
Looks interesting!
Unfortunately using GPL license would limit it's usage. Something like an MPL-2 might preserve copyleft of the library but not a whole program. Unless of course you're planning to offer commercial licenses.