incognitolm commited on
Commit
bb2c539
ยท
1 Parent(s): e6654de

ui and stuff

Browse files
public/app.js CHANGED
@@ -2490,6 +2490,9 @@
2490
  state.pagePayload = payload;
2491
  document.title = routeTitle(payload.page);
2492
  updateActiveNav();
 
 
 
2493
  root.innerHTML = renderPage(payload.page);
2494
  root.setAttribute("aria-busy", "false");
2495
  attachPageEffects(payload.page);
 
2490
  state.pagePayload = payload;
2491
  document.title = routeTitle(payload.page);
2492
  updateActiveNav();
2493
+ root.dataset.pageKind = payload.page?.kind || "";
2494
+ root.dataset.pageTab = payload.page?.tab || "";
2495
+ root.dataset.resourceType = payload.page?.resourceType || "";
2496
  root.innerHTML = renderPage(payload.page);
2497
  root.setAttribute("aria-busy", "false");
2498
  attachPageEffects(payload.page);
public/styles.css CHANGED
@@ -1175,3 +1175,890 @@ button {
1175
  padding: 16px 18px 0;
1176
  }
1177
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1175
  padding: 16px 18px 0;
1176
  }
1177
  }
1178
+
1179
+ /* Professional workspace relayout */
1180
+
1181
+ :root {
1182
+ --bg: #e7edf2;
1183
+ --surface: #ffffff;
1184
+ --surface-muted: #f2f5f8;
1185
+ --surface-soft: #f8fafc;
1186
+ --ink: #111827;
1187
+ --muted: #556273;
1188
+ --line: #c7d1dc;
1189
+ --line-strong: #9ba9b8;
1190
+ --accent: #a96d08;
1191
+ --accent-soft: #f5e4c1;
1192
+ --success: #0f6d48;
1193
+ --warning: #915e06;
1194
+ --danger: #aa3c2b;
1195
+ --blue: #2457a6;
1196
+ --shadow: 0 16px 38px rgba(17, 24, 39, 0.06);
1197
+ --radius-xl: 6px;
1198
+ --radius-lg: 5px;
1199
+ --radius-md: 4px;
1200
+ --radius-sm: 3px;
1201
+ }
1202
+
1203
+ html,
1204
+ body {
1205
+ font-family: "Aptos", "Segoe UI Variable Text", "Segoe UI", "Helvetica Neue", sans-serif;
1206
+ background:
1207
+ linear-gradient(180deg, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.8)),
1208
+ linear-gradient(rgba(17, 24, 39, 0.03) 1px, transparent 1px),
1209
+ linear-gradient(90deg, rgba(17, 24, 39, 0.03) 1px, transparent 1px),
1210
+ var(--bg);
1211
+ background-size: auto, 32px 32px, 32px 32px, auto;
1212
+ }
1213
+
1214
+ body {
1215
+ letter-spacing: 0.01em;
1216
+ }
1217
+
1218
+ a:focus-visible,
1219
+ button:focus-visible,
1220
+ input:focus-visible,
1221
+ select:focus-visible,
1222
+ textarea:focus-visible {
1223
+ outline: 2px solid rgba(36, 87, 166, 0.42);
1224
+ outline-offset: 2px;
1225
+ }
1226
+
1227
+ .mono,
1228
+ .stream-output,
1229
+ .file-editor,
1230
+ .file-content {
1231
+ font-family: "Cascadia Mono", "IBM Plex Mono", "SFMono-Regular", Consolas, monospace;
1232
+ }
1233
+
1234
+ .login-panel,
1235
+ .panel,
1236
+ .hero-panel,
1237
+ .content-section,
1238
+ .page-header,
1239
+ .loading-panel {
1240
+ border-radius: var(--radius-md);
1241
+ border-color: var(--line-strong);
1242
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(249, 251, 252, 0.96));
1243
+ box-shadow: var(--shadow);
1244
+ }
1245
+
1246
+ .login-shell {
1247
+ width: min(860px, 100%);
1248
+ }
1249
+
1250
+ .login-panel {
1251
+ display: grid;
1252
+ gap: 18px;
1253
+ padding: 36px;
1254
+ border-top: 5px solid var(--ink);
1255
+ }
1256
+
1257
+ .login-copy h1 {
1258
+ font-size: clamp(30px, 4vw, 48px);
1259
+ max-width: 15ch;
1260
+ }
1261
+
1262
+ .eyebrow {
1263
+ color: var(--blue);
1264
+ font-size: 11px;
1265
+ letter-spacing: 0.14em;
1266
+ }
1267
+
1268
+ .page-shell[data-page-kind="space"] .eyebrow,
1269
+ .page-shell[data-page-kind="model"] .eyebrow,
1270
+ .page-shell[data-page-kind="dataset"] .eyebrow,
1271
+ .page-shell[data-page-kind="bucket"] .eyebrow {
1272
+ color: var(--accent);
1273
+ }
1274
+
1275
+ .login-form input,
1276
+ .search-input,
1277
+ .branch-select,
1278
+ .file-editor,
1279
+ .text-input,
1280
+ .form-textarea {
1281
+ border-color: var(--line-strong);
1282
+ border-radius: var(--radius-md);
1283
+ background: #fff;
1284
+ }
1285
+
1286
+ .primary-button,
1287
+ .secondary-button,
1288
+ .danger-button,
1289
+ .ghost-button,
1290
+ .menu-button,
1291
+ .menu-link,
1292
+ .user-trigger {
1293
+ min-height: 44px;
1294
+ border-radius: var(--radius-md);
1295
+ font-weight: 600;
1296
+ }
1297
+
1298
+ .primary-button {
1299
+ background: #18212d;
1300
+ }
1301
+
1302
+ .primary-button:hover,
1303
+ .user-trigger:hover {
1304
+ background: #223041;
1305
+ }
1306
+
1307
+ .secondary-button,
1308
+ .danger-button,
1309
+ .ghost-button,
1310
+ .menu-button,
1311
+ .menu-link,
1312
+ .user-trigger {
1313
+ border-color: var(--line-strong);
1314
+ background: #fff;
1315
+ }
1316
+
1317
+ .danger-button {
1318
+ background: #fff2f0;
1319
+ border-color: #e7b4aa;
1320
+ color: #a83d2d;
1321
+ }
1322
+
1323
+ .create-type-button.is-active {
1324
+ background: #18212d;
1325
+ border-color: #18212d;
1326
+ }
1327
+
1328
+ .app-body {
1329
+ padding: 18px;
1330
+ }
1331
+
1332
+ .app-shell {
1333
+ width: min(1500px, 100%);
1334
+ }
1335
+
1336
+ .topbar {
1337
+ top: 18px;
1338
+ display: grid;
1339
+ grid-template-columns: minmax(0, 1fr) auto;
1340
+ gap: 16px;
1341
+ padding: 14px 18px;
1342
+ border-radius: var(--radius-md);
1343
+ border: 1px solid var(--line-strong);
1344
+ background: rgba(253, 254, 255, 0.98);
1345
+ backdrop-filter: none;
1346
+ }
1347
+
1348
+ .brand-row {
1349
+ display: grid;
1350
+ grid-template-columns: auto minmax(0, 1fr);
1351
+ gap: 18px;
1352
+ align-items: center;
1353
+ }
1354
+
1355
+ .brand-text-lockup {
1356
+ font-size: 14px;
1357
+ font-weight: 800;
1358
+ letter-spacing: 0.12em;
1359
+ text-transform: uppercase;
1360
+ }
1361
+
1362
+ .nav-links {
1363
+ gap: 0;
1364
+ flex-wrap: nowrap;
1365
+ overflow: auto;
1366
+ border: 1px solid var(--line);
1367
+ background: #edf2f7;
1368
+ }
1369
+
1370
+ .nav-links a {
1371
+ min-height: 40px;
1372
+ padding: 0 16px;
1373
+ border-right: 1px solid var(--line);
1374
+ border-radius: 0;
1375
+ color: var(--muted);
1376
+ font-size: 14px;
1377
+ font-weight: 700;
1378
+ white-space: nowrap;
1379
+ }
1380
+
1381
+ .nav-links a:last-child {
1382
+ border-right: 0;
1383
+ }
1384
+
1385
+ .nav-links a:hover,
1386
+ .nav-links a.is-active {
1387
+ background: #fff;
1388
+ color: var(--ink);
1389
+ box-shadow: inset 0 -2px 0 var(--accent);
1390
+ }
1391
+
1392
+ .toolbar {
1393
+ justify-self: end;
1394
+ gap: 12px;
1395
+ }
1396
+
1397
+ .search-shell {
1398
+ width: min(34vw, 420px);
1399
+ }
1400
+
1401
+ .search-results,
1402
+ .user-popover {
1403
+ border-radius: var(--radius-md);
1404
+ border-color: var(--line-strong);
1405
+ box-shadow: var(--shadow);
1406
+ }
1407
+
1408
+ .search-result-row,
1409
+ .menu-link,
1410
+ .menu-button {
1411
+ border-radius: var(--radius-md);
1412
+ }
1413
+
1414
+ .flash-stack {
1415
+ top: 88px;
1416
+ }
1417
+
1418
+ .flash-message {
1419
+ border-radius: var(--radius-md);
1420
+ }
1421
+
1422
+ .page-shell {
1423
+ margin-top: 16px;
1424
+ gap: 16px;
1425
+ }
1426
+
1427
+ .hero-panel,
1428
+ .content-section,
1429
+ .page-header,
1430
+ .panel,
1431
+ .loading-panel {
1432
+ padding: 22px 24px;
1433
+ }
1434
+
1435
+ .page-header {
1436
+ align-items: end;
1437
+ border-left: 4px solid var(--ink);
1438
+ }
1439
+
1440
+ .page-shell[data-page-kind="settings"] .page-header,
1441
+ .page-shell[data-page-kind="collection"] .page-header {
1442
+ border-left-color: var(--blue);
1443
+ }
1444
+
1445
+ .page-shell[data-page-kind="space"] .page-header,
1446
+ .page-shell[data-page-kind="model"] .page-header,
1447
+ .page-shell[data-page-kind="dataset"] .page-header,
1448
+ .page-shell[data-page-kind="bucket"] .page-header {
1449
+ border-left-color: var(--accent);
1450
+ }
1451
+
1452
+ .hero-panel {
1453
+ grid-template-columns: minmax(0, 1.35fr) minmax(320px, 0.95fr);
1454
+ gap: 18px;
1455
+ align-items: stretch;
1456
+ border-top: 4px solid var(--accent);
1457
+ }
1458
+
1459
+ .page-header h1,
1460
+ .hero-copy h1,
1461
+ .section-head h2,
1462
+ .panel-head h2 {
1463
+ letter-spacing: -0.02em;
1464
+ }
1465
+
1466
+ .hero-copy p,
1467
+ .page-subtitle,
1468
+ .body-copy,
1469
+ .plain-list {
1470
+ max-width: 72ch;
1471
+ }
1472
+
1473
+ .meta-badge {
1474
+ min-height: 28px;
1475
+ padding: 0 10px;
1476
+ border-radius: 2px;
1477
+ font-size: 11px;
1478
+ font-weight: 700;
1479
+ letter-spacing: 0.08em;
1480
+ text-transform: uppercase;
1481
+ background: #f6f8fa;
1482
+ }
1483
+
1484
+ .meta-badge.accent {
1485
+ border-color: #e6c78e;
1486
+ }
1487
+
1488
+ .button-row,
1489
+ .meta-badge-row {
1490
+ gap: 8px;
1491
+ }
1492
+
1493
+ .content-grid {
1494
+ display: grid;
1495
+ grid-template-columns: repeat(12, minmax(0, 1fr));
1496
+ gap: 16px;
1497
+ }
1498
+
1499
+ .content-grid > * {
1500
+ min-width: 0;
1501
+ grid-column: 1 / -1;
1502
+ }
1503
+
1504
+ .content-grid.two-up > * {
1505
+ grid-column: span 6;
1506
+ }
1507
+
1508
+ .content-grid.two-up > :only-child,
1509
+ .full-span {
1510
+ grid-column: 1 / -1;
1511
+ }
1512
+
1513
+ .files-grid > article {
1514
+ grid-column: span 8;
1515
+ }
1516
+
1517
+ .files-grid > aside {
1518
+ grid-column: span 4;
1519
+ }
1520
+
1521
+ .stats-grid {
1522
+ display: grid;
1523
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1524
+ gap: 12px;
1525
+ width: 100%;
1526
+ }
1527
+
1528
+ .stats-grid.compact {
1529
+ margin-top: 16px;
1530
+ }
1531
+
1532
+ .stat-card {
1533
+ min-height: 0;
1534
+ padding: 16px;
1535
+ border-radius: var(--radius-md);
1536
+ background: #fff;
1537
+ border: 1px solid var(--line);
1538
+ border-top: 3px solid var(--ink);
1539
+ }
1540
+
1541
+ .stat-card:nth-child(2n) {
1542
+ border-top-color: var(--blue);
1543
+ }
1544
+
1545
+ .stat-card:nth-child(3n) {
1546
+ border-top-color: var(--accent);
1547
+ }
1548
+
1549
+ .stat-label {
1550
+ font-size: 11px;
1551
+ font-weight: 700;
1552
+ letter-spacing: 0.08em;
1553
+ text-transform: uppercase;
1554
+ }
1555
+
1556
+ .stat-value {
1557
+ margin-top: 10px;
1558
+ font-size: clamp(24px, 3vw, 34px);
1559
+ line-height: 1.1;
1560
+ }
1561
+
1562
+ .page-shell[data-page-kind="space"][data-page-tab="app"] .stat-value,
1563
+ .page-shell[data-page-kind="space"][data-page-tab="settings"] .stat-value {
1564
+ font-size: clamp(18px, 2vw, 26px);
1565
+ }
1566
+
1567
+ .section-head,
1568
+ .panel-head {
1569
+ margin-bottom: 16px;
1570
+ padding-bottom: 12px;
1571
+ border-bottom: 1px solid var(--line);
1572
+ }
1573
+
1574
+ .panel-head.between {
1575
+ align-items: end;
1576
+ }
1577
+
1578
+ .section-head a {
1579
+ font-size: 12px;
1580
+ font-weight: 700;
1581
+ letter-spacing: 0.08em;
1582
+ text-transform: uppercase;
1583
+ }
1584
+
1585
+ .resource-list,
1586
+ .activity-list,
1587
+ .mini-grid,
1588
+ .plain-list,
1589
+ .detail-list,
1590
+ .file-entry-list,
1591
+ .badge-grid,
1592
+ .settings-link-list,
1593
+ .settings-item-list {
1594
+ gap: 10px;
1595
+ }
1596
+
1597
+ .page-shell[data-page-kind="dashboard"] .resource-list,
1598
+ .page-shell[data-page-kind="collection"] .resource-list {
1599
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1600
+ }
1601
+
1602
+ .resource-row,
1603
+ .mini-card,
1604
+ .activity-item,
1605
+ .file-entry-row,
1606
+ .settings-link-row,
1607
+ .settings-item-card {
1608
+ border-radius: var(--radius-md);
1609
+ background: #fff;
1610
+ border-color: var(--line);
1611
+ box-shadow: none;
1612
+ }
1613
+
1614
+ .resource-row,
1615
+ .file-entry-row {
1616
+ display: grid;
1617
+ grid-template-columns: minmax(0, 1fr) auto;
1618
+ align-items: start;
1619
+ min-height: 0;
1620
+ padding: 16px 18px;
1621
+ }
1622
+
1623
+ .resource-row:hover,
1624
+ .file-entry-row:hover,
1625
+ .settings-link-row:hover,
1626
+ .external-row:hover {
1627
+ background: #fff;
1628
+ border-color: var(--line-strong);
1629
+ box-shadow: inset 0 0 0 1px var(--line-strong);
1630
+ }
1631
+
1632
+ .resource-row-title,
1633
+ .file-entry-title,
1634
+ .activity-item-title {
1635
+ font-size: 17px;
1636
+ }
1637
+
1638
+ .resource-row-subtitle,
1639
+ .file-entry-meta,
1640
+ .resource-row-meta,
1641
+ .activity-item-meta,
1642
+ .mini-card-subtitle,
1643
+ .search-result-meta,
1644
+ .settings-item-meta,
1645
+ .settings-link-arrow,
1646
+ .resource-row-updated {
1647
+ font-size: 13px;
1648
+ }
1649
+
1650
+ .resource-row-side,
1651
+ .file-entry-side,
1652
+ .resource-row-updated,
1653
+ .settings-link-arrow {
1654
+ color: var(--muted);
1655
+ font-weight: 700;
1656
+ letter-spacing: 0.08em;
1657
+ text-transform: uppercase;
1658
+ }
1659
+
1660
+ .activity-item,
1661
+ .settings-link-row {
1662
+ min-height: 0;
1663
+ padding: 16px 18px;
1664
+ }
1665
+
1666
+ .settings-link-row {
1667
+ align-items: start;
1668
+ }
1669
+
1670
+ .mini-card {
1671
+ min-height: 0;
1672
+ padding: 16px 18px;
1673
+ }
1674
+
1675
+ .mini-grid {
1676
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
1677
+ }
1678
+
1679
+ .badge-grid {
1680
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
1681
+ }
1682
+
1683
+ .page-header,
1684
+ .hero-panel,
1685
+ .content-section {
1686
+ overflow: hidden;
1687
+ }
1688
+
1689
+ .section-tabs {
1690
+ gap: 0;
1691
+ flex-wrap: nowrap;
1692
+ overflow: auto;
1693
+ margin-top: -2px;
1694
+ padding: 0;
1695
+ border: 1px solid var(--line-strong);
1696
+ border-radius: var(--radius-md);
1697
+ background: #edf2f7;
1698
+ }
1699
+
1700
+ .section-tabs a {
1701
+ min-height: 42px;
1702
+ padding: 0 16px;
1703
+ border: 0;
1704
+ border-right: 1px solid var(--line);
1705
+ border-radius: 0;
1706
+ background: transparent;
1707
+ color: var(--muted);
1708
+ font-weight: 700;
1709
+ white-space: nowrap;
1710
+ }
1711
+
1712
+ .section-tabs a:last-child {
1713
+ border-right: 0;
1714
+ }
1715
+
1716
+ .section-tabs a.is-active {
1717
+ background: #fff;
1718
+ color: var(--ink);
1719
+ border-color: transparent;
1720
+ box-shadow: inset 0 -2px 0 var(--accent);
1721
+ }
1722
+
1723
+ .inline-notice,
1724
+ .empty-state,
1725
+ .error-banner {
1726
+ border-radius: var(--radius-md);
1727
+ border-left: 4px solid var(--line-strong);
1728
+ background: #f4f7fa;
1729
+ }
1730
+
1731
+ .inline-notice.tone-warning {
1732
+ border-left-color: #d5a44d;
1733
+ }
1734
+
1735
+ .error-banner {
1736
+ border-left-color: #d65a4c;
1737
+ }
1738
+
1739
+ .detail-list div {
1740
+ display: grid;
1741
+ grid-template-columns: minmax(120px, 180px) minmax(0, 1fr);
1742
+ align-items: start;
1743
+ }
1744
+
1745
+ .detail-list dd {
1746
+ justify-self: end;
1747
+ text-align: right;
1748
+ }
1749
+
1750
+ .plain-list li + li {
1751
+ margin-top: 10px;
1752
+ }
1753
+
1754
+ .settings-item-card {
1755
+ padding: 16px 18px;
1756
+ }
1757
+
1758
+ .settings-item-value {
1759
+ margin-top: 12px;
1760
+ padding-top: 12px;
1761
+ border-top: 1px solid var(--line);
1762
+ }
1763
+
1764
+ .field-grid.two-col,
1765
+ .settings-split-grid {
1766
+ gap: 12px;
1767
+ }
1768
+
1769
+ .danger-panel {
1770
+ border-left: 4px solid #d65a4c;
1771
+ border-top: 0;
1772
+ }
1773
+
1774
+ .files-layout {
1775
+ display: grid;
1776
+ grid-template-columns: minmax(260px, 320px) minmax(0, 1fr);
1777
+ gap: 16px;
1778
+ margin-top: 16px;
1779
+ }
1780
+
1781
+ .files-column {
1782
+ min-width: 0;
1783
+ }
1784
+
1785
+ .breadcrumbs {
1786
+ margin-top: 16px;
1787
+ padding: 10px 12px;
1788
+ border: 1px solid var(--line);
1789
+ border-radius: var(--radius-md);
1790
+ background: #f6f8fa;
1791
+ }
1792
+
1793
+ .breadcrumb-divider {
1794
+ color: var(--line-strong);
1795
+ }
1796
+
1797
+ .file-preview-wrap {
1798
+ gap: 12px;
1799
+ }
1800
+
1801
+ .editor-meta {
1802
+ padding: 10px 12px;
1803
+ border: 1px solid var(--line);
1804
+ border-radius: var(--radius-md);
1805
+ background: #f6f8fa;
1806
+ }
1807
+
1808
+ .file-editor {
1809
+ min-height: 360px;
1810
+ }
1811
+
1812
+ .file-content {
1813
+ min-height: 360px;
1814
+ padding: 18px;
1815
+ border-radius: var(--radius-md);
1816
+ border: 1px solid var(--line-strong);
1817
+ background: #fff;
1818
+ }
1819
+
1820
+ .console-panel {
1821
+ border-top: 4px solid #26374e;
1822
+ padding-bottom: 0;
1823
+ }
1824
+
1825
+ .stream-toolbar {
1826
+ display: grid;
1827
+ grid-template-columns: minmax(0, 1fr) auto;
1828
+ gap: 12px;
1829
+ align-items: center;
1830
+ margin-top: 16px;
1831
+ padding: 14px 0 16px;
1832
+ }
1833
+
1834
+ .stream-tab-row {
1835
+ display: grid;
1836
+ grid-template-columns: repeat(4, minmax(0, 1fr));
1837
+ gap: 0;
1838
+ overflow: hidden;
1839
+ border: 1px solid #32455c;
1840
+ background: #142031;
1841
+ }
1842
+
1843
+ .stream-tab-button {
1844
+ min-height: 44px;
1845
+ padding: 0 16px;
1846
+ border: 0;
1847
+ border-right: 1px solid #32455c;
1848
+ border-radius: 0;
1849
+ background: transparent;
1850
+ color: #d8e1ec;
1851
+ justify-content: flex-start;
1852
+ font-size: 12px;
1853
+ font-weight: 700;
1854
+ letter-spacing: 0.08em;
1855
+ text-transform: uppercase;
1856
+ }
1857
+
1858
+ .stream-tab-button:last-child {
1859
+ border-right: 0;
1860
+ }
1861
+
1862
+ .stream-tab-button:hover {
1863
+ background: #1b2940;
1864
+ color: #fff;
1865
+ }
1866
+
1867
+ .stream-tab-button.is-active {
1868
+ background: #21324b;
1869
+ border-color: #32455c;
1870
+ color: #fff;
1871
+ box-shadow: inset 0 -2px 0 var(--accent);
1872
+ }
1873
+
1874
+ .stream-caption,
1875
+ .stream-panel-copy {
1876
+ font-size: 12px;
1877
+ letter-spacing: 0.04em;
1878
+ }
1879
+
1880
+ .stream-caption {
1881
+ white-space: nowrap;
1882
+ }
1883
+
1884
+ .stream-tab-panels {
1885
+ margin: 0 -24px;
1886
+ background: #0b1220;
1887
+ }
1888
+
1889
+ .stream-panel-meta {
1890
+ display: grid;
1891
+ grid-template-columns: auto minmax(0, 1fr);
1892
+ gap: 10px 16px;
1893
+ align-items: end;
1894
+ padding: 16px 24px 12px;
1895
+ color: #d8e2f0;
1896
+ border-bottom: 1px solid #223043;
1897
+ }
1898
+
1899
+ .stream-panel-title {
1900
+ color: #f8fbff;
1901
+ font-size: 12px;
1902
+ font-weight: 700;
1903
+ letter-spacing: 0.1em;
1904
+ text-transform: uppercase;
1905
+ }
1906
+
1907
+ .stream-panel-copy {
1908
+ justify-self: end;
1909
+ color: #98aac0;
1910
+ text-align: right;
1911
+ }
1912
+
1913
+ .stream-output {
1914
+ min-height: 420px;
1915
+ padding: 18px 24px 24px 70px;
1916
+ background:
1917
+ linear-gradient(
1918
+ 90deg,
1919
+ rgba(148, 163, 184, 0.08) 0,
1920
+ rgba(148, 163, 184, 0.08) 48px,
1921
+ rgba(61, 81, 109, 0.9) 48px,
1922
+ rgba(61, 81, 109, 0.9) 49px,
1923
+ transparent 49px
1924
+ ),
1925
+ repeating-linear-gradient(
1926
+ 180deg,
1927
+ rgba(148, 163, 184, 0.08) 0,
1928
+ rgba(148, 163, 184, 0.08) 1px,
1929
+ transparent 1px,
1930
+ transparent 24px
1931
+ ),
1932
+ linear-gradient(180deg, #0d1522, #0a111b);
1933
+ color: #d8e2f0;
1934
+ font-size: 13px;
1935
+ line-height: 1.55;
1936
+ tab-size: 2;
1937
+ }
1938
+
1939
+ .page-shell[data-page-tab="files"] .activity-list,
1940
+ .page-shell[data-page-tab="community"] .activity-list {
1941
+ align-content: start;
1942
+ }
1943
+
1944
+ @media (max-width: 1200px) {
1945
+ .topbar {
1946
+ grid-template-columns: 1fr;
1947
+ }
1948
+
1949
+ .toolbar {
1950
+ justify-self: stretch;
1951
+ }
1952
+
1953
+ .search-shell {
1954
+ width: 100%;
1955
+ }
1956
+
1957
+ .content-grid.two-up > * {
1958
+ grid-column: 1 / -1;
1959
+ }
1960
+
1961
+ .files-grid > article,
1962
+ .files-grid > aside {
1963
+ grid-column: 1 / -1;
1964
+ }
1965
+
1966
+ .page-shell[data-page-kind="dashboard"] .resource-list,
1967
+ .page-shell[data-page-kind="collection"] .resource-list {
1968
+ grid-template-columns: 1fr;
1969
+ }
1970
+ }
1971
+
1972
+ @media (max-width: 980px) {
1973
+ .hero-panel,
1974
+ .files-layout {
1975
+ grid-template-columns: 1fr;
1976
+ }
1977
+
1978
+ .stream-toolbar {
1979
+ grid-template-columns: 1fr;
1980
+ }
1981
+
1982
+ .stream-caption {
1983
+ white-space: normal;
1984
+ }
1985
+
1986
+ .stream-panel-copy {
1987
+ justify-self: start;
1988
+ text-align: left;
1989
+ }
1990
+ }
1991
+
1992
+ @media (max-width: 720px) {
1993
+ .app-body {
1994
+ padding: 12px;
1995
+ }
1996
+
1997
+ .topbar,
1998
+ .login-panel,
1999
+ .hero-panel,
2000
+ .page-header,
2001
+ .content-section,
2002
+ .panel,
2003
+ .loading-panel {
2004
+ padding: 18px;
2005
+ }
2006
+
2007
+ .brand-row {
2008
+ grid-template-columns: 1fr;
2009
+ }
2010
+
2011
+ .nav-links {
2012
+ flex-wrap: wrap;
2013
+ }
2014
+
2015
+ .nav-links a {
2016
+ border-right: 0;
2017
+ border-bottom: 1px solid var(--line);
2018
+ }
2019
+
2020
+ .section-tabs {
2021
+ flex-wrap: wrap;
2022
+ }
2023
+
2024
+ .section-tabs a {
2025
+ flex: 1 1 48%;
2026
+ border-right: 0;
2027
+ border-bottom: 1px solid var(--line);
2028
+ }
2029
+
2030
+ .stats-grid,
2031
+ .mini-grid,
2032
+ .badge-grid,
2033
+ .page-shell[data-page-kind="dashboard"] .resource-list,
2034
+ .page-shell[data-page-kind="collection"] .resource-list {
2035
+ grid-template-columns: 1fr;
2036
+ }
2037
+
2038
+ .stream-tab-row {
2039
+ grid-template-columns: repeat(2, minmax(0, 1fr));
2040
+ }
2041
+
2042
+ .stream-tab-panels {
2043
+ margin: 0 -18px;
2044
+ }
2045
+
2046
+ .stream-panel-meta {
2047
+ grid-template-columns: 1fr;
2048
+ padding: 14px 18px 10px;
2049
+ }
2050
+
2051
+ .stream-output {
2052
+ min-height: 320px;
2053
+ padding: 16px 18px 20px 60px;
2054
+ }
2055
+
2056
+ .detail-list div {
2057
+ grid-template-columns: 1fr;
2058
+ }
2059
+
2060
+ .detail-list dd {
2061
+ justify-self: start;
2062
+ text-align: left;
2063
+ }
2064
+ }
server.js CHANGED
@@ -106,6 +106,46 @@ function shouldUseSecureCookies() {
106
  return process.env.NODE_ENV === "production" || process.env.COOKIE_SECURE === "true";
107
  }
108
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  function redirect(res, location) {
110
  res.writeHead(302, {
111
  ...SECURITY_HEADERS,
@@ -302,6 +342,17 @@ async function handleAuthFailure(res, session) {
302
  }
303
 
304
  async function handleLogin(req, res) {
 
 
 
 
 
 
 
 
 
 
 
305
  const rawBody = await readBody(req);
306
  const form = parseFormBody(rawBody);
307
  const accessToken = (form.accessToken || "").trim();
 
106
  return process.env.NODE_ENV === "production" || process.env.COOKIE_SECURE === "true";
107
  }
108
 
109
+ function getExpectedOrigin(req) {
110
+ const forwardedProto = String(req.headers["x-forwarded-proto"] || "")
111
+ .split(",")[0]
112
+ .trim()
113
+ .toLowerCase();
114
+ const protocol = forwardedProto || (req.socket.encrypted ? "https" : "http");
115
+ const host = String(req.headers.host || "").trim();
116
+ return host ? `${protocol}://${host}` : "";
117
+ }
118
+
119
+ function matchesTrustedOrigin(req) {
120
+ const expectedOrigin = getExpectedOrigin(req);
121
+ if (!expectedOrigin) {
122
+ return false;
123
+ }
124
+
125
+ const origin = String(req.headers.origin || "").trim();
126
+ if (origin) {
127
+ return origin === expectedOrigin;
128
+ }
129
+
130
+ const referer = String(req.headers.referer || "").trim();
131
+ if (referer) {
132
+ try {
133
+ return new URL(referer).origin === expectedOrigin;
134
+ } catch (error) {
135
+ return false;
136
+ }
137
+ }
138
+
139
+ const fetchSite = String(req.headers["sec-fetch-site"] || "")
140
+ .trim()
141
+ .toLowerCase();
142
+ if (fetchSite) {
143
+ return fetchSite === "same-origin" || fetchSite === "same-site" || fetchSite === "none";
144
+ }
145
+
146
+ return false;
147
+ }
148
+
149
  function redirect(res, location) {
150
  res.writeHead(302, {
151
  ...SECURITY_HEADERS,
 
342
  }
343
 
344
  async function handleLogin(req, res) {
345
+ if (!matchesTrustedOrigin(req)) {
346
+ sendHtml(
347
+ res,
348
+ 403,
349
+ renderLoginPage({
350
+ loginError: "That sign-in attempt was blocked because the request origin did not match this workspace.",
351
+ }),
352
+ );
353
+ return;
354
+ }
355
+
356
  const rawBody = await readBody(req);
357
  const form = parseFormBody(rawBody);
358
  const accessToken = (form.accessToken || "").trim();
test/auth-flow.test.js CHANGED
@@ -182,10 +182,28 @@ async function run() {
182
  const appAddress = appServer.address();
183
  const baseUrl = `http://127.0.0.1:${appAddress.port}`;
184
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  const loginResponse = await fetch(`${baseUrl}/auth/login`, {
186
  method: "POST",
187
  headers: {
188
  "Content-Type": "application/x-www-form-urlencoded",
 
189
  },
190
  body: new URLSearchParams({
191
  accessToken: "hf_test_token",
 
182
  const appAddress = appServer.address();
183
  const baseUrl = `http://127.0.0.1:${appAddress.port}`;
184
 
185
+ const blockedLoginResponse = await fetch(`${baseUrl}/auth/login`, {
186
+ method: "POST",
187
+ headers: {
188
+ "Content-Type": "application/x-www-form-urlencoded",
189
+ Origin: "http://evil.example",
190
+ },
191
+ body: new URLSearchParams({
192
+ accessToken: "hf_test_token",
193
+ }),
194
+ redirect: "manual",
195
+ });
196
+
197
+ const blockedHtml = await blockedLoginResponse.text();
198
+ assert.equal(blockedLoginResponse.status, 403);
199
+ assert.equal(blockedLoginResponse.headers.get("set-cookie"), null);
200
+ assert.match(blockedHtml, /request origin did not match/i);
201
+
202
  const loginResponse = await fetch(`${baseUrl}/auth/login`, {
203
  method: "POST",
204
  headers: {
205
  "Content-Type": "application/x-www-form-urlencoded",
206
+ Origin: baseUrl,
207
  },
208
  body: new URLSearchParams({
209
  accessToken: "hf_test_token",
test/bucket-pages.test.js CHANGED
@@ -152,6 +152,7 @@ async function login(baseUrl) {
152
  method: "POST",
153
  headers: {
154
  "Content-Type": "application/x-www-form-urlencoded",
 
155
  },
156
  body: new URLSearchParams({
157
  accessToken: "hf_test_token",
 
152
  method: "POST",
153
  headers: {
154
  "Content-Type": "application/x-www-form-urlencoded",
155
+ Origin: baseUrl,
156
  },
157
  body: new URLSearchParams({
158
  accessToken: "hf_test_token",
test/resource-mutations.test.js CHANGED
@@ -115,6 +115,7 @@ async function loginAndGetSession(baseUrl) {
115
  method: "POST",
116
  headers: {
117
  "Content-Type": "application/x-www-form-urlencoded",
 
118
  },
119
  body: new URLSearchParams({
120
  accessToken: "hf_test_token",
 
115
  method: "POST",
116
  headers: {
117
  "Content-Type": "application/x-www-form-urlencoded",
118
+ Origin: baseUrl,
119
  },
120
  body: new URLSearchParams({
121
  accessToken: "hf_test_token",
test/space-settings.test.js CHANGED
@@ -138,6 +138,7 @@ async function login(baseUrl) {
138
  method: "POST",
139
  headers: {
140
  "Content-Type": "application/x-www-form-urlencoded",
 
141
  },
142
  body: new URLSearchParams({
143
  accessToken: "hf_test_token",
 
138
  method: "POST",
139
  headers: {
140
  "Content-Type": "application/x-www-form-urlencoded",
141
+ Origin: baseUrl,
142
  },
143
  body: new URLSearchParams({
144
  accessToken: "hf_test_token",