violit 0.0.2__py3-none-any.whl → 0.0.4__py3-none-any.whl
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.
- violit/app.py +121 -117
- violit/engine.py +15 -2
- violit/widgets/chat_widgets.py +2 -1
- violit-0.0.4.dist-info/METADATA +397 -0
- {violit-0.0.2.dist-info → violit-0.0.4.dist-info}/RECORD +8 -8
- {violit-0.0.2.dist-info → violit-0.0.4.dist-info}/WHEEL +1 -1
- {violit-0.0.2.dist-info → violit-0.0.4.dist-info}/licenses/LICENSE +1 -1
- violit-0.0.2.dist-info/METADATA +0 -504
- {violit-0.0.2.dist-info → violit-0.0.4.dist-info}/top_level.txt +0 -0
violit/app.py
CHANGED
|
@@ -271,8 +271,16 @@ class App(
|
|
|
271
271
|
def state(self, default_value, key=None) -> State:
|
|
272
272
|
"""Create a reactive state variable"""
|
|
273
273
|
if key is None:
|
|
274
|
-
|
|
275
|
-
|
|
274
|
+
# Streamlit-style: Generate stable key from caller's location
|
|
275
|
+
frame = inspect.currentframe()
|
|
276
|
+
try:
|
|
277
|
+
caller_frame = frame.f_back
|
|
278
|
+
filename = os.path.basename(caller_frame.f_code.co_filename)
|
|
279
|
+
lineno = caller_frame.f_lineno
|
|
280
|
+
# Create stable key: filename_linenumber
|
|
281
|
+
name = f"state_{filename}_{lineno}"
|
|
282
|
+
finally:
|
|
283
|
+
del frame # Avoid reference cycles
|
|
276
284
|
else:
|
|
277
285
|
name = key
|
|
278
286
|
return State(name, default_value)
|
|
@@ -861,6 +869,7 @@ class App(
|
|
|
861
869
|
t = store['theme']
|
|
862
870
|
|
|
863
871
|
sidebar_style = "" if (sidebar_c or self.static_sidebar_order) else "display: none;"
|
|
872
|
+
main_class = "" if (sidebar_c or self.static_sidebar_order) else "sidebar-collapsed"
|
|
864
873
|
|
|
865
874
|
# Generate CSRF token
|
|
866
875
|
# Get sid from context (set by middleware) instead of cookies (not set yet on first visit)
|
|
@@ -880,7 +889,7 @@ class App(
|
|
|
880
889
|
# Debug flag injection
|
|
881
890
|
debug_script = f'<script>window._debug_mode = {str(self.debug_mode).lower()};</script>'
|
|
882
891
|
|
|
883
|
-
html = HTML_TEMPLATE.replace("%CONTENT%", main_c).replace("%SIDEBAR_CONTENT%", sidebar_c).replace("%SIDEBAR_STYLE%", sidebar_style).replace("%MODE%", self.mode).replace("%TITLE%", self.app_title).replace("%THEME_CLASS%", t.theme_class).replace("%CSS_VARS%", t.to_css_vars()).replace("%SPLASH%", self._splash_html if self.show_splash else "").replace("%CONTAINER_MAX_WIDTH%", self.container_max_width).replace("%CSRF_SCRIPT%", csrf_script).replace("%DEBUG_SCRIPT%", debug_script)
|
|
892
|
+
html = HTML_TEMPLATE.replace("%CONTENT%", main_c).replace("%SIDEBAR_CONTENT%", sidebar_c).replace("%SIDEBAR_STYLE%", sidebar_style).replace("%MAIN_CLASS%", main_class).replace("%MODE%", self.mode).replace("%TITLE%", self.app_title).replace("%THEME_CLASS%", t.theme_class).replace("%CSS_VARS%", t.to_css_vars()).replace("%SPLASH%", self._splash_html if self.show_splash else "").replace("%CONTAINER_MAX_WIDTH%", self.container_max_width).replace("%CSRF_SCRIPT%", csrf_script).replace("%DEBUG_SCRIPT%", debug_script)
|
|
884
893
|
return HTMLResponse(html)
|
|
885
894
|
|
|
886
895
|
@self.fastapi.post("/action/{cid}")
|
|
@@ -1036,9 +1045,12 @@ class App(
|
|
|
1036
1045
|
|
|
1037
1046
|
self.debug_print(f" Action found: {act is not None}")
|
|
1038
1047
|
|
|
1048
|
+
# Detect if this is a navigation action (nav menu click)
|
|
1049
|
+
is_navigation = cid.startswith('nav_menu')
|
|
1050
|
+
|
|
1039
1051
|
if act:
|
|
1040
1052
|
store['eval_queue'] = []
|
|
1041
|
-
self.debug_print(f" Executing action for CID: {cid}...")
|
|
1053
|
+
self.debug_print(f" Executing action for CID: {cid} (navigation={is_navigation})...")
|
|
1042
1054
|
act(v) if v is not None else act()
|
|
1043
1055
|
self.debug_print(f" Action executed")
|
|
1044
1056
|
|
|
@@ -1050,9 +1062,10 @@ class App(
|
|
|
1050
1062
|
self.debug_print(f" Dirty components: {len(dirty)} ({[c.id for c in dirty]})")
|
|
1051
1063
|
|
|
1052
1064
|
# Send all dirty components via WebSocket
|
|
1065
|
+
# Pass is_navigation flag to enable/disable smooth transitions
|
|
1053
1066
|
if dirty:
|
|
1054
|
-
self.debug_print(f" Sending {len(dirty)} updates via WebSocket...")
|
|
1055
|
-
await self.ws_engine.push_updates(sid, dirty)
|
|
1067
|
+
self.debug_print(f" Sending {len(dirty)} updates via WebSocket (navigation={is_navigation})...")
|
|
1068
|
+
await self.ws_engine.push_updates(sid, dirty, is_navigation=is_navigation)
|
|
1056
1069
|
self.debug_print(f" [OK] Updates sent successfully")
|
|
1057
1070
|
else:
|
|
1058
1071
|
self.debug_print(f" [!] No dirty components found - nothing to update")
|
|
@@ -1398,6 +1411,7 @@ HTML_TEMPLATE = """
|
|
|
1398
1411
|
<style>
|
|
1399
1412
|
:root {
|
|
1400
1413
|
%CSS_VARS%
|
|
1414
|
+
--sidebar-width: 300px;
|
|
1401
1415
|
}
|
|
1402
1416
|
sl-alert { --sl-color-primary-500: var(--sl-primary); --sl-color-primary-600: var(--sl-primary); }
|
|
1403
1417
|
sl-alert::part(base) { border: 1px solid var(--sl-border); }
|
|
@@ -1417,31 +1431,39 @@ HTML_TEMPLATE = """
|
|
|
1417
1431
|
|
|
1418
1432
|
#root { display: flex; width: 100%; min-height: 100vh; }
|
|
1419
1433
|
#sidebar {
|
|
1420
|
-
|
|
1434
|
+
position: fixed;
|
|
1435
|
+
top: 0;
|
|
1436
|
+
left: 0;
|
|
1437
|
+
width: var(--sidebar-width);
|
|
1438
|
+
height: 100vh;
|
|
1421
1439
|
background: var(--sl-bg-card);
|
|
1422
1440
|
border-right: 1px solid var(--sl-border);
|
|
1423
1441
|
padding: 2rem 1rem;
|
|
1424
1442
|
display: flex;
|
|
1425
1443
|
flex-direction: column;
|
|
1426
1444
|
gap: 1rem;
|
|
1427
|
-
flex-shrink: 0;
|
|
1428
1445
|
overflow-y: auto;
|
|
1429
1446
|
overflow-x: hidden;
|
|
1430
|
-
white-space: nowrap;
|
|
1431
|
-
|
|
1432
|
-
top: 0;
|
|
1433
|
-
height: 100vh;
|
|
1447
|
+
white-space: nowrap;
|
|
1448
|
+
z-index: 1100;
|
|
1434
1449
|
}
|
|
1435
1450
|
#sidebar.collapsed { width: 0; padding: 2rem 0; border-right: none; opacity: 0; }
|
|
1436
1451
|
|
|
1437
1452
|
#main {
|
|
1438
1453
|
flex: 1;
|
|
1454
|
+
margin-left: var(--sidebar-width);
|
|
1439
1455
|
display: flex;
|
|
1440
1456
|
flex-direction: column;
|
|
1441
1457
|
align-items: center;
|
|
1442
|
-
padding: 0 1.5rem 3rem
|
|
1443
|
-
transition: padding 0.3s ease;
|
|
1458
|
+
padding: 0 1.5rem 3rem 2.5rem;
|
|
1459
|
+
transition: margin-left 0.3s ease, padding 0.3s ease;
|
|
1444
1460
|
}
|
|
1461
|
+
#main.sidebar-collapsed { margin-left: 0; }
|
|
1462
|
+
/* Chat input container positioning - respects sidebar */
|
|
1463
|
+
.chat-input-container { left: var(--sidebar-width) !important; transition: left 0.3s ease; }
|
|
1464
|
+
#sidebar.collapsed ~ #main .chat-input-container,
|
|
1465
|
+
#main.sidebar-collapsed .chat-input-container { left: 0 !important; }
|
|
1466
|
+
|
|
1445
1467
|
#header { width: 100%; max-width: %CONTAINER_MAX_WIDTH%; padding: 1rem 0; display: flex; align-items: center; }
|
|
1446
1468
|
#app { width: 100%; max-width: %CONTAINER_MAX_WIDTH%; display: flex; flex-direction: column; gap: 1.5rem; }
|
|
1447
1469
|
|
|
@@ -1658,116 +1680,87 @@ HTML_TEMPLATE = """
|
|
|
1658
1680
|
debugLog("[WebSocket] Message received");
|
|
1659
1681
|
const msg = JSON.parse(e.data);
|
|
1660
1682
|
if(msg.type === 'update') {
|
|
1661
|
-
//
|
|
1662
|
-
|
|
1663
|
-
const
|
|
1664
|
-
|
|
1665
|
-
msg.payload.forEach(item => {
|
|
1666
|
-
// Only page_renderer gets smooth transition
|
|
1667
|
-
if (item.id.startsWith('page_renderer')) {
|
|
1668
|
-
pageUpdates.push(item);
|
|
1669
|
-
} else {
|
|
1670
|
-
regularUpdates.push(item);
|
|
1671
|
-
}
|
|
1672
|
-
});
|
|
1683
|
+
// Check if this is a navigation update (page transition)
|
|
1684
|
+
// Server sends isNavigation flag based on action type
|
|
1685
|
+
const isNavigation = msg.isNavigation === true;
|
|
1673
1686
|
|
|
1674
|
-
//
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
if (isSelfOrChild || isShadowChild) {
|
|
1684
|
-
// Check if it's actually an input that needs protection
|
|
1685
|
-
const tag = document.activeElement.tagName.toLowerCase();
|
|
1686
|
-
const isInput = tag === 'input' || tag === 'textarea' || tag.startsWith('sl-input') || tag.startsWith('sl-textarea');
|
|
1687
|
+
// Helper function to apply updates
|
|
1688
|
+
const applyUpdates = (items) => {
|
|
1689
|
+
items.forEach(item => {
|
|
1690
|
+
const el = document.getElementById(item.id);
|
|
1691
|
+
|
|
1692
|
+
// Focus Guard: Skip update if element is focused input to prevent interrupting typing
|
|
1693
|
+
if (document.activeElement && el) {
|
|
1694
|
+
const isSelfOrChild = document.activeElement.id === item.id || el.contains(document.activeElement);
|
|
1695
|
+
const isShadowChild = document.activeElement.closest && document.activeElement.closest(`#${item.id}`);
|
|
1687
1696
|
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1697
|
+
if (isSelfOrChild || isShadowChild) {
|
|
1698
|
+
// Check if it's actually an input that needs protection
|
|
1699
|
+
const tag = document.activeElement.tagName.toLowerCase();
|
|
1700
|
+
const isInput = tag === 'input' || tag === 'textarea' || tag.startsWith('sl-input') || tag.startsWith('sl-textarea');
|
|
1701
|
+
|
|
1702
|
+
// If it's an input, block update. If it's a button (nav menu), ALLOW update.
|
|
1703
|
+
if (isInput) {
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1691
1706
|
}
|
|
1692
|
-
|
|
1693
|
-
}
|
|
1707
|
+
}
|
|
1694
1708
|
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
// Checkbox/Toggle: Update checked property only (preserve animation)
|
|
1701
|
-
if (widgetType === 'checkbox' || widgetType === 'toggle') {
|
|
1702
|
-
// Parse new HTML to extract checked state
|
|
1703
|
-
const temp = document.createElement('div');
|
|
1704
|
-
temp.innerHTML = item.html;
|
|
1705
|
-
const newCheckbox = temp.querySelector('sl-checkbox, sl-switch');
|
|
1709
|
+
if(el) {
|
|
1710
|
+
// Smart update for specific widget types to preserve animations/instances
|
|
1711
|
+
const widgetType = item.id.split('_')[0];
|
|
1712
|
+
let smartUpdated = false;
|
|
1706
1713
|
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1714
|
+
// Checkbox/Toggle: Update checked property only (preserve animation)
|
|
1715
|
+
if (widgetType === 'checkbox' || widgetType === 'toggle') {
|
|
1716
|
+
// Parse new HTML to extract checked state
|
|
1717
|
+
const temp = document.createElement('div');
|
|
1718
|
+
temp.innerHTML = item.html;
|
|
1719
|
+
const newCheckbox = temp.querySelector('sl-checkbox, sl-switch');
|
|
1712
1720
|
|
|
1713
|
-
if (
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1721
|
+
if (newCheckbox) {
|
|
1722
|
+
// Find the actual checkbox element (may be direct or nested)
|
|
1723
|
+
const checkboxEl = el.tagName && (el.tagName.toLowerCase() === 'sl-checkbox' || el.tagName.toLowerCase() === 'sl-switch')
|
|
1724
|
+
? el
|
|
1725
|
+
: el.querySelector('sl-checkbox, sl-switch');
|
|
1726
|
+
|
|
1727
|
+
if (checkboxEl) {
|
|
1728
|
+
const shouldBeChecked = newCheckbox.hasAttribute('checked');
|
|
1729
|
+
// Only update if different to avoid interrupting user interaction
|
|
1730
|
+
if (checkboxEl.checked !== shouldBeChecked) {
|
|
1731
|
+
checkboxEl.checked = shouldBeChecked;
|
|
1732
|
+
}
|
|
1733
|
+
smartUpdated = true;
|
|
1718
1734
|
}
|
|
1719
|
-
smartUpdated = true;
|
|
1720
1735
|
}
|
|
1721
1736
|
}
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1737
|
+
|
|
1738
|
+
// Data Editor: Update AG Grid data only
|
|
1739
|
+
if (widgetType === 'data' && item.id.includes('editor')) {
|
|
1740
|
+
// item.id is like "data_editor_xxx_wrapper", extract base cid
|
|
1741
|
+
const baseCid = item.id.replace('_wrapper', '');
|
|
1742
|
+
const gridApi = window['gridApi_' + baseCid];
|
|
1743
|
+
if (gridApi) {
|
|
1744
|
+
// Extract rowData from new HTML
|
|
1745
|
+
const match = item.html.match(/rowData:\s*(\[.*?\])/s);
|
|
1746
|
+
if (match) {
|
|
1747
|
+
try {
|
|
1748
|
+
const newData = JSON.parse(match[1]);
|
|
1749
|
+
gridApi.setRowData(newData);
|
|
1750
|
+
smartUpdated = true;
|
|
1751
|
+
} catch (e) {
|
|
1752
|
+
console.error('Failed to parse AG Grid data:', e);
|
|
1753
|
+
}
|
|
1739
1754
|
}
|
|
1740
1755
|
}
|
|
1741
1756
|
}
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
// Default: Full DOM replacement
|
|
1745
|
-
if (!smartUpdated) {
|
|
1746
|
-
purgePlotly(el);
|
|
1747
|
-
el.outerHTML = item.html;
|
|
1748
1757
|
|
|
1749
|
-
//
|
|
1750
|
-
|
|
1751
|
-
temp.innerHTML = item.html;
|
|
1752
|
-
temp.querySelectorAll('script').forEach(s => {
|
|
1753
|
-
const script = document.createElement('script');
|
|
1754
|
-
script.textContent = s.textContent;
|
|
1755
|
-
document.body.appendChild(script);
|
|
1756
|
-
script.remove();
|
|
1757
|
-
});
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
});
|
|
1761
|
-
|
|
1762
|
-
// Page updates: use View Transitions if available
|
|
1763
|
-
if (pageUpdates.length > 0) {
|
|
1764
|
-
const updatePages = () => {
|
|
1765
|
-
pageUpdates.forEach(item => {
|
|
1766
|
-
const el = document.getElementById(item.id);
|
|
1767
|
-
if(el) {
|
|
1758
|
+
// Default: Full DOM replacement
|
|
1759
|
+
if (!smartUpdated) {
|
|
1768
1760
|
purgePlotly(el);
|
|
1769
1761
|
el.outerHTML = item.html;
|
|
1770
1762
|
|
|
1763
|
+
// Execute scripts
|
|
1771
1764
|
const temp = document.createElement('div');
|
|
1772
1765
|
temp.innerHTML = item.html;
|
|
1773
1766
|
temp.querySelectorAll('script').forEach(s => {
|
|
@@ -1777,14 +1770,17 @@ HTML_TEMPLATE = """
|
|
|
1777
1770
|
script.remove();
|
|
1778
1771
|
});
|
|
1779
1772
|
}
|
|
1780
|
-
}
|
|
1781
|
-
};
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1773
|
+
}
|
|
1774
|
+
});
|
|
1775
|
+
};
|
|
1776
|
+
|
|
1777
|
+
// Apply updates: use View Transitions ONLY for navigation
|
|
1778
|
+
if (isNavigation && document.body.classList.contains('anim-soft') && document.startViewTransition) {
|
|
1779
|
+
// Navigation: smooth page transition
|
|
1780
|
+
document.startViewTransition(() => applyUpdates(msg.payload));
|
|
1781
|
+
} else {
|
|
1782
|
+
// Regular updates: apply immediately without animation for snappy response
|
|
1783
|
+
applyUpdates(msg.payload);
|
|
1788
1784
|
}
|
|
1789
1785
|
} else if (msg.type === 'eval') {
|
|
1790
1786
|
const func = new Function(msg.code);
|
|
@@ -1839,8 +1835,16 @@ HTML_TEMPLATE = """
|
|
|
1839
1835
|
|
|
1840
1836
|
function toggleSidebar() {
|
|
1841
1837
|
const sb = document.getElementById('sidebar');
|
|
1838
|
+
const main = document.getElementById('main');
|
|
1839
|
+
const chatInput = document.querySelector('.chat-input-container');
|
|
1842
1840
|
sb.classList.toggle('collapsed');
|
|
1841
|
+
main.classList.toggle('sidebar-collapsed');
|
|
1842
|
+
// Also adjust chat input container if present
|
|
1843
|
+
if (chatInput) {
|
|
1844
|
+
chatInput.style.left = sb.classList.contains('collapsed') ? '0' : '300px';
|
|
1845
|
+
}
|
|
1843
1846
|
}
|
|
1847
|
+
|
|
1844
1848
|
function createToast(message, variant = 'primary', icon = 'info-circle') {
|
|
1845
1849
|
const variantColors = { primary: '#0ea5e9', success: '#10b981', warning: '#f59e0b', danger: '#ef4444' };
|
|
1846
1850
|
const toast = document.createElement('div');
|
|
@@ -1971,7 +1975,7 @@ HTML_TEMPLATE = """
|
|
|
1971
1975
|
<div id="sidebar" style="%SIDEBAR_STYLE%">
|
|
1972
1976
|
%SIDEBAR_CONTENT%
|
|
1973
1977
|
</div>
|
|
1974
|
-
<div id="main">
|
|
1978
|
+
<div id="main" class="%MAIN_CLASS%">
|
|
1975
1979
|
<div id="header">
|
|
1976
1980
|
<sl-icon-button name="list" style="font-size: 1.5rem; color: var(--sl-text);" onclick="toggleSidebar()"></sl-icon-button>
|
|
1977
1981
|
</div>
|
violit/engine.py
CHANGED
|
@@ -23,11 +23,24 @@ class WsEngine:
|
|
|
23
23
|
def click_attrs(self, cid: str):
|
|
24
24
|
return {"onclick": f"window.sendAction('{cid}')"}
|
|
25
25
|
|
|
26
|
-
async def push_updates(self, sid: str, components: List[Component]):
|
|
26
|
+
async def push_updates(self, sid: str, components: List[Component], is_navigation: bool = False):
|
|
27
|
+
"""Push component updates to client
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
sid: Session ID
|
|
31
|
+
components: List of components to update
|
|
32
|
+
is_navigation: If True, apply smooth page transition animation.
|
|
33
|
+
If False (default), update immediately without animation.
|
|
34
|
+
"""
|
|
27
35
|
if sid in self.sockets:
|
|
28
36
|
payload = [{"id": c.id, "html": c.render()} for c in components]
|
|
29
|
-
await self.sockets[sid].send_json({
|
|
37
|
+
await self.sockets[sid].send_json({
|
|
38
|
+
"type": "update",
|
|
39
|
+
"payload": payload,
|
|
40
|
+
"isNavigation": is_navigation # Flag for client to determine animation
|
|
41
|
+
})
|
|
30
42
|
|
|
31
43
|
async def push_eval(self, sid: str, code: str):
|
|
32
44
|
if sid in self.sockets:
|
|
33
45
|
await self.sockets[sid].send_json({"type": "eval", "code": code})
|
|
46
|
+
|
violit/widgets/chat_widgets.py
CHANGED
|
@@ -116,7 +116,7 @@ class ChatWidgetsMixin:
|
|
|
116
116
|
<div class="chat-input-container" style="
|
|
117
117
|
position: fixed;
|
|
118
118
|
bottom: 0;
|
|
119
|
-
left:
|
|
119
|
+
left: 300px;
|
|
120
120
|
right: 0;
|
|
121
121
|
padding: 20px;
|
|
122
122
|
background: linear-gradient(to top, var(--sl-bg) 80%, transparent);
|
|
@@ -124,6 +124,7 @@ class ChatWidgetsMixin:
|
|
|
124
124
|
display: flex;
|
|
125
125
|
justify-content: center;
|
|
126
126
|
pointer-events: none;
|
|
127
|
+
transition: left 0.3s ease;
|
|
127
128
|
">
|
|
128
129
|
<div style="
|
|
129
130
|
width: 100%;
|