@package-uploader/ui 1.1.1 → 1.1.3

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.
@@ -1 +1 @@
1
- :root{--color-primary: #2563eb;--color-primary-hover: #1d4ed8;--color-success: #10b981;--color-error: #ef4444;--color-warning: #f59e0b;--color-bg: #ffffff;--color-bg-secondary: #f9fafb;--color-border: #e5e7eb;--color-text: #111827;--color-text-muted: #6b7280;--radius: 8px;--radius-sm: 4px;--shadow: 0 1px 3px rgba(0, 0, 0, .1);--shadow-lg: 0 4px 6px rgba(0, 0, 0, .1)}*{box-sizing:border-box;margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,sans-serif;background:var(--color-bg-secondary);color:var(--color-text);line-height:1.5}.app{min-height:100vh;display:flex;flex-direction:column}.header{background:var(--color-bg);border-bottom:1px solid var(--color-border);padding:1rem 2rem;display:flex;align-items:center;justify-content:space-between}.header h1{font-size:1.25rem;font-weight:600}.nav{display:flex;gap:1rem}.nav a{color:var(--color-text-muted);text-decoration:none;padding:.5rem 1rem;border-radius:var(--radius);transition:all .2s}.nav a:hover,.nav a.active{color:var(--color-primary);background:#2563eb1a}.main{flex:1;padding:2rem;max-width:1200px;margin:0 auto;width:100%}.card{background:var(--color-bg);border:1px solid var(--color-border);border-radius:var(--radius);padding:1.5rem;box-shadow:var(--shadow)}.card+.card{margin-top:1rem}.card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem}.card-title{font-size:1.125rem;font-weight:600}.btn{display:inline-flex;align-items:center;gap:.5rem;padding:.5rem 1rem;border-radius:var(--radius);border:none;font-size:.875rem;font-weight:500;cursor:pointer;transition:all .2s}.btn-primary{background:var(--color-primary);color:#fff}.btn-primary:hover{background:var(--color-primary-hover)}.btn-secondary{background:var(--color-bg);border:1px solid var(--color-border);color:var(--color-text)}.btn-secondary:hover{background:var(--color-bg-secondary)}.btn:disabled{opacity:.5;cursor:not-allowed}.dropzone{border:2px dashed var(--color-border);border-radius:var(--radius);padding:3rem;text-align:center;cursor:pointer;transition:all .2s}.dropzone:hover,.dropzone.active{border-color:var(--color-primary);background:#2563eb0d}.dropzone p{color:var(--color-text-muted);margin-top:.5rem}.file-list{margin-top:1rem}.file-item{display:flex;align-items:center;justify-content:space-between;padding:.75rem;border:1px solid var(--color-border);border-radius:var(--radius);margin-bottom:.5rem}.file-item .name{font-weight:500}.file-item .size{color:var(--color-text-muted);font-size:.875rem}.file-item .status{display:flex;align-items:center;gap:.5rem}.status-pending{color:var(--color-text-muted)}.status-uploading{color:var(--color-primary)}.status-success{color:var(--color-success)}.status-error{color:var(--color-error)}.document-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1rem}.document-card{background:var(--color-bg);border:1px solid var(--color-border);border-radius:var(--radius);padding:1rem;transition:all .2s}.document-card:hover{box-shadow:var(--shadow-lg)}.document-card .title{font-weight:600;margin-bottom:.25rem}.document-card .meta{color:var(--color-text-muted);font-size:.875rem;margin-bottom:.75rem}.document-card .actions{display:flex;gap:.5rem}.folder-tree{border:1px solid var(--color-border);border-radius:var(--radius);padding:1rem;max-height:400px;overflow-y:auto}.folder-item{padding:.5rem;cursor:pointer;border-radius:var(--radius);display:flex;align-items:center;gap:.5rem}.folder-item:hover{background:var(--color-bg-secondary)}.folder-item.selected{background:#2563eb1a;color:var(--color-primary)}.folder-item .icon{font-size:1.25rem}.modal-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1000}.modal{background:var(--color-bg);border-radius:var(--radius);padding:1.5rem;max-width:500px;width:90%;max-height:80vh;overflow-y:auto}.modal-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem}.modal-title{font-size:1.125rem;font-weight:600}.modal-close{background:none;border:none;font-size:1.5rem;cursor:pointer;color:var(--color-text-muted)}.form-group{margin-bottom:1rem}.form-label{display:block;font-weight:500;margin-bottom:.25rem}.form-input,.form-select{width:100%;padding:.5rem;border:1px solid var(--color-border);border-radius:var(--radius);font-size:1rem}.form-input:focus,.form-select:focus{outline:none;border-color:var(--color-primary)}.spinner{width:20px;height:20px;border:2px solid var(--color-border);border-top-color:var(--color-primary);border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.empty-state{text-align:center;padding:3rem;color:var(--color-text-muted)}.empty-state h3{color:var(--color-text);margin-bottom:.5rem}.alert{padding:1rem;border-radius:var(--radius);margin-bottom:1rem}.alert-success{background:#10b9811a;border:1px solid var(--color-success);color:var(--color-success)}.alert-error{background:#ef44441a;border:1px solid var(--color-error);color:var(--color-error)}.alert-warning{background:#f59e0b1a;border:1px solid var(--color-warning);color:var(--color-warning)}.breadcrumb{display:flex;align-items:center;flex-wrap:wrap;gap:0;font-size:.875rem}.breadcrumb-item{background:none;border:none;color:var(--color-primary);cursor:pointer;padding:.25rem .5rem;border-radius:var(--radius);font-size:inherit}.breadcrumb-item:hover{background:#2563eb1a}.breadcrumb-current{color:var(--color-text);cursor:default}.breadcrumb-current:hover{background:transparent}.breadcrumb-separator{color:var(--color-text-muted);margin:0 .25rem}.browse-page{display:flex;flex-direction:column;gap:1rem}.toolbar{display:flex;justify-content:space-between;align-items:center;background:var(--color-bg);border:1px solid var(--color-border);border-radius:var(--radius);padding:.75rem 1rem;gap:1rem;flex-wrap:wrap}.toolbar-left{display:flex;align-items:center;gap:.5rem;flex:1;min-width:200px}.toolbar-right{display:flex;align-items:center;gap:.5rem}.search-input{padding:.5rem .75rem;border:1px solid var(--color-border);border-radius:var(--radius);font-size:.875rem;width:200px}.search-input:focus{outline:none;border-color:var(--color-primary)}.content-area{background:var(--color-bg);border:1px solid var(--color-border);border-radius:var(--radius);overflow:hidden}.data-grid-container{display:flex;flex-direction:column}.data-grid{width:100%;border-collapse:collapse;font-size:.875rem}.data-grid thead{background:var(--color-bg-secondary);border-bottom:1px solid var(--color-border)}.data-grid th{text-align:left;padding:.75rem 1rem;font-weight:600;color:var(--color-text-muted);white-space:nowrap}.data-grid th.sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.data-grid th.sortable:hover{color:var(--color-text)}.data-grid td{padding:.75rem 1rem;border-bottom:1px solid var(--color-border);vertical-align:middle}.grid-row{transition:background .15s}.grid-row:hover{background:var(--color-bg-secondary)}.folder-row{cursor:pointer}.folder-row:hover{background:#2563eb0d}.col-checkbox{width:40px;text-align:center}.col-name{min-width:250px}.col-format,.col-type{width:120px}.col-updated,.col-created{width:140px}.col-actions{width:100px;text-align:right}.item-icon{margin-right:.5rem;font-size:1rem}.item-name{font-weight:500}.subfolder-count{color:var(--color-text-muted);font-size:.75rem;margin-left:.5rem}.btn-icon{background:none;border:none;padding:.25rem .5rem;cursor:pointer;font-size:1rem;opacity:.6;transition:opacity .15s}.btn-icon:hover{opacity:1}.btn-icon:disabled{opacity:.3;cursor:not-allowed}.btn-icon.pinned,.btn-delete:hover{opacity:1}.data-grid-footer{padding:.75rem 1rem;background:var(--color-bg-secondary);border-top:1px solid var(--color-border);font-size:.75rem;color:var(--color-text-muted)}.data-grid-loading,.data-grid-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:3rem;text-align:center;color:var(--color-text-muted);gap:.5rem}.data-grid-empty .empty-icon{font-size:3rem;margin-bottom:.5rem}.data-grid-empty h3{color:var(--color-text);margin:0}.data-grid-empty p{margin:.5rem 0 1rem}.folder-browser{border:1px solid var(--color-border);border-radius:var(--radius);overflow:hidden}.folder-browser-toolbar{display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;background:var(--color-bg-secondary);border-bottom:1px solid var(--color-border)}.folder-browser-actions{display:flex;gap:.5rem}.folder-browser-list{max-height:300px;overflow-y:auto}.folder-browser-table{width:100%;border-collapse:collapse}.folder-browser-row{transition:background .15s}.folder-browser-row:hover{background:var(--color-bg-secondary)}.folder-browser-row.selected{background:#2563eb1a}.folder-browser-row td{padding:.5rem;border-bottom:1px solid var(--color-border);vertical-align:middle}.folder-browser-icon{width:40px;text-align:center;font-size:1.25rem}.folder-browser-name{cursor:pointer;font-weight:500}.folder-browser-name:hover{color:var(--color-primary)}.subfolder-indicator{color:var(--color-text-muted);font-size:.75rem;font-weight:400;margin-left:.5rem}.folder-browser-select{width:100px;text-align:center}.folder-browser-delete{width:50px;text-align:center}.folder-browser-loading,.folder-browser-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem;color:var(--color-text-muted);gap:.75rem}.upload-page{display:flex;flex-direction:column;gap:1rem}.selected-folder-badge{background:#2563eb1a;color:var(--color-primary);padding:.25rem .75rem;border-radius:999px;font-size:.75rem;font-weight:500}.upload-actions{margin-top:1rem;display:flex;gap:.5rem;justify-content:flex-end}.btn-sm{padding:.25rem .5rem;font-size:.75rem}.document-row{cursor:pointer;transition:background .15s}.document-row:hover{background:var(--color-bg-secondary)}.document-row.expanded{background:#2563eb0d}.expand-icon{display:inline-block;width:1rem;margin-right:.25rem;transition:transform .2s;color:var(--color-text-muted)}.expand-icon.expanded{transform:rotate(90deg)}.expanded-row td{padding:0!important;background:var(--color-bg-secondary);border-bottom:2px solid var(--color-border)}.course-detail-panel{padding:1rem;display:flex;flex-direction:column;gap:1rem}.course-detail-loading,.course-detail-error{display:flex;align-items:center;justify-content:center;gap:.75rem;padding:2rem;color:var(--color-text-muted)}.course-detail-error{color:var(--color-error);flex-direction:column}.course-detail-tile{background:var(--color-bg);border:1px solid var(--color-border);border-radius:var(--radius);padding:1rem}.tile-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem;padding-bottom:.75rem;border-bottom:1px solid var(--color-border)}.tile-header h3{font-size:1rem;font-weight:600;margin:0}.btn-close{background:none;border:none;font-size:1.25rem;color:var(--color-text-muted);cursor:pointer;padding:.25rem;line-height:1;border-radius:var(--radius);transition:all .15s}.btn-close:hover{background:var(--color-bg-secondary);color:var(--color-text)}.detail-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:.75rem 1.5rem}.detail-item{display:flex;flex-direction:column;gap:.125rem}.detail-item.span-2{grid-column:span 2}.detail-label{font-size:.7rem;font-weight:600;color:var(--color-text-muted);text-transform:uppercase;letter-spacing:.025em}.detail-value{font-size:.875rem;color:var(--color-text)}.detail-value.mono{font-family:SF Mono,Monaco,Consolas,monospace;font-size:.75rem;background:var(--color-bg-secondary);padding:.125rem .375rem;border-radius:4px}.detail-value.description{line-height:1.4;max-height:4.2em;overflow:hidden;text-overflow:ellipsis}.thin-pack-row{display:grid;grid-template-columns:repeat(2,1fr);gap:1rem}.thin-pack-tile{background:var(--color-bg);border:1px solid var(--color-border);border-radius:var(--radius);padding:1rem;border-left:4px solid}.thin-pack-tile.public{border-left-color:var(--color-success)}.thin-pack-tile.private{border-left-color:var(--color-primary)}.thin-pack-header{display:flex;align-items:flex-start;gap:.75rem;margin-bottom:.75rem}.thin-pack-icon{font-size:1.5rem}.thin-pack-titles{flex:1}.thin-pack-title{font-size:.9rem;font-weight:600;margin:0}.thin-pack-subtitle{font-size:.75rem;color:var(--color-text-muted);margin:.125rem 0 0}.thin-pack-content{display:flex;flex-direction:column;gap:.5rem}.thin-pack-error{font-size:.75rem;color:var(--color-error);padding:.5rem;background:#ef44441a;border-radius:4px}.thin-pack-status{display:flex;align-items:center;gap:.5rem;font-size:.8rem}.thin-pack-status.exists{color:var(--color-success)}.thin-pack-status.exists .status-icon{display:inline-flex;align-items:center;justify-content:center;width:1.25rem;height:1.25rem;background:var(--color-success);color:#fff;border-radius:50%;font-size:.7rem}.thin-pack-status.missing{color:var(--color-text-muted);font-style:italic}.thin-pack-meta{display:flex;gap:1rem;font-size:.7rem;color:var(--color-text-muted)}.thin-pack-actions{display:flex;gap:.5rem;margin-top:.25rem}.thin-pack-slug-input{display:flex;flex-direction:column;gap:.25rem}.thin-pack-slug-input label{font-size:.7rem;color:var(--color-text-muted)}.thin-pack-slug-input input{padding:.375rem .5rem;font-size:.8rem;border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-bg);color:var(--color-text)}.thin-pack-slug-input input:focus{outline:none;border-color:var(--color-primary)}.thin-pack-slug-input input::placeholder{color:var(--color-text-muted);opacity:.6}.thin-pack-name-input{display:flex;flex-direction:column;gap:.25rem}.thin-pack-name-input label{font-size:.7rem;color:var(--color-text-muted)}.thin-pack-name-input input{padding:.375rem .5rem;font-size:.8rem;border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-bg);color:var(--color-text)}.thin-pack-name-input input:focus{outline:none;border-color:var(--color-primary)}.thin-pack-name-input input::placeholder{color:var(--color-text-muted);opacity:.6}.token-preview{font-size:.65rem;color:var(--color-text-muted);font-family:monospace;word-break:break-all}.thin-pack-visibility{display:flex;flex-direction:column;gap:.25rem;padding:.5rem;background:var(--color-bg-tertiary);border-radius:4px}.visibility-toggle{display:flex;align-items:center;gap:.5rem;cursor:pointer}.visibility-toggle input[type=checkbox]{width:16px;height:16px;cursor:pointer}.toggle-label{font-size:.85rem;font-weight:500}.visibility-hint{font-size:.7rem;color:var(--color-text-muted);margin-left:24px}.thin-pack-tile.lms{border-left-color:var(--color-success)}.thin-pack-tile.shared{border-left-color:var(--color-primary)}.upload-modal{width:600px;max-width:90vw;max-height:90vh;display:flex;flex-direction:column}.upload-modal-content{flex:1;overflow-y:auto;padding:1rem;display:flex;flex-direction:column;gap:1rem}.upload-modal .dropzone{min-height:120px}.upload-file-list{display:flex;flex-direction:column;gap:.5rem}.upload-file-list h4{margin:0;font-size:.85rem;color:var(--color-text-muted)}.upload-file-item{display:flex;justify-content:space-between;align-items:center;padding:.5rem;background:var(--color-bg-secondary);border-radius:8px}.upload-file-info{display:flex;flex-direction:column;gap:.125rem}.upload-file-name{font-weight:500;font-size:.875rem}.upload-file-size{font-size:.75rem;color:var(--color-text-muted)}.upload-file-status{display:flex;align-items:center;gap:.5rem}.upload-progress-container{display:flex;align-items:center;gap:.75rem;min-width:150px}.upload-progress-bar{flex:1;height:8px;background:var(--color-border);border-radius:4px;overflow:hidden}.upload-progress-fill{height:100%;background:var(--color-primary);transition:width .15s ease-out}.upload-progress-text{font-size:.75rem;font-weight:500;color:var(--color-text-muted);min-width:36px;text-align:right}.status-processing{color:var(--color-primary);font-size:.875rem}.upload-link-options{background:var(--color-bg-secondary);border-radius:var(--radius);padding:1rem;display:flex;flex-direction:column;gap:.75rem}.upload-link-options h4{margin:0;font-size:.85rem;color:var(--color-text-muted)}.upload-link-option{display:flex;flex-direction:column;gap:.5rem}.upload-link-toggle{display:flex;align-items:center;gap:.5rem;cursor:pointer}.upload-link-toggle input[type=checkbox]{width:1rem;height:1rem;cursor:pointer}.upload-link-toggle span{font-size:.875rem}.upload-link-name-input{display:flex;flex-direction:column;gap:.25rem;padding-left:1.5rem}.upload-link-name-input input{padding:.375rem .5rem;font-size:.8rem;border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-bg);color:var(--color-text)}.upload-link-name-input input:focus{outline:none;border-color:var(--color-primary)}.upload-link-name-input input::placeholder{color:var(--color-text-muted);opacity:.6}.slug-preview{font-size:.7rem;color:var(--color-text-muted);font-family:monospace}.modal-footer{display:flex;justify-content:flex-end;gap:.5rem;padding:1rem;border-top:1px solid var(--color-border)}@media (max-width: 768px){.detail-grid{grid-template-columns:1fr}.detail-item.span-2{grid-column:span 1}.thin-pack-row{grid-template-columns:1fr}}.user-menu{display:flex;align-items:center;gap:.75rem;margin-left:auto}.user-info{display:flex;align-items:center;gap:.5rem}.user-avatar{width:2rem;height:2rem;border-radius:50%;background:var(--color-primary);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:600;font-size:.875rem}.user-name{font-size:.875rem;color:var(--color-text);max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.header{display:flex;align-items:center;gap:1.5rem}.header .nav{margin-right:auto}.course-structure-step{display:flex;flex-direction:column;gap:.5rem}.course-structure-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:.25rem}.course-structure-header h4{margin:0;font-size:1rem;font-weight:600}.course-structure-meta{display:flex;align-items:center;gap:.5rem;font-size:.75rem;color:var(--color-text-muted)}.course-structure-badge{background:#2563eb1a;color:var(--color-primary);padding:.125rem .5rem;border-radius:999px;font-size:.7rem;font-weight:500}.tree-node{padding:.25rem 0}.tree-node-row{display:flex;align-items:center;gap:.375rem;min-height:1.5rem}.tree-node-icon{font-size:1rem;flex-shrink:0}.tree-node-label{font-weight:500;font-size:.875rem;flex:1}.tree-class-input{width:100%;padding:.25rem .5rem;font-size:.8rem;border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-bg);color:var(--color-text);margin-top:.25rem}.tree-class-input:focus{outline:none;border-color:var(--color-primary)}.tree-class-input::placeholder{color:var(--color-text-muted);opacity:.6}.tree-class-input-sm{max-width:200px;margin-top:0;margin-left:auto;flex-shrink:0}.tree-course{background:var(--color-bg-secondary);border:1px solid var(--color-border);border-radius:var(--radius);padding:.5rem .75rem}.tree-lessons{display:flex;flex-direction:column;gap:.125rem;max-height:400px;overflow-y:auto;border:1px solid var(--color-border);border-radius:var(--radius);padding:.25rem}.tree-lesson{border-bottom:1px solid var(--color-border)}.tree-lesson:last-child{border-bottom:none}.tree-node-lesson{cursor:pointer;padding:.375rem .5rem;border-radius:var(--radius-sm);transition:background .15s}.tree-node-lesson:hover{background:var(--color-bg-secondary)}.tree-expand{font-size:.625rem;width:1rem;text-align:center;color:var(--color-text-muted);flex-shrink:0}.tree-lesson-num{color:var(--color-text-muted);font-weight:600;font-size:.75rem}.tree-node-indicator{font-size:.65rem;color:var(--color-primary);background:#2563eb1a;padding:.0625rem .375rem;border-radius:999px;white-space:nowrap;flex-shrink:0}.tree-lesson-content{padding:.25rem .5rem .5rem 1.5rem}.tree-lesson-class{margin-bottom:.375rem}.tree-node-block{display:flex;align-items:center;gap:.375rem;padding:.1875rem 0}.tree-node-block .tree-node-row{flex:1;min-width:0}.tree-block-pipe{font-family:monospace;color:var(--color-border);font-size:.75rem;flex-shrink:0}.tree-block-type{font-size:.75rem;font-weight:500;color:var(--color-text);white-space:nowrap}.tree-block-title{font-size:.7rem;color:var(--color-text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:200px}.structure-parsing-indicator{display:flex;align-items:center;gap:.75rem;padding:.75rem;color:var(--color-primary);font-size:.875rem}.structure-detected-badge{background:#10b9811a;color:var(--color-success);border:1px solid var(--color-success);border-radius:var(--radius);padding:.5rem .75rem;font-size:.8rem}.structure-multi-note{font-size:.75rem;color:var(--color-text-muted);font-style:italic;padding:.25rem 0}.structure-summary-badge{background:#2563eb1a;color:var(--color-primary);border-radius:var(--radius);padding:.5rem .75rem;font-size:.8rem;margin-top:.25rem}.structure-section{display:flex;flex-direction:column}.structure-toggle-btn{display:flex;align-items:center;gap:.5rem;width:100%;padding:.625rem .75rem;background:var(--color-bg);border:1px solid var(--color-border);border-radius:var(--radius);cursor:pointer;font-size:.875rem;font-weight:500;color:var(--color-text);transition:all .2s}.structure-toggle-btn:hover:not(:disabled){border-color:var(--color-primary);background:#2563eb08}.structure-toggle-btn.open{border-color:var(--color-primary);border-bottom-left-radius:0;border-bottom-right-radius:0;background:#2563eb0d}.structure-toggle-btn.disabled{opacity:.5;cursor:not-allowed;color:var(--color-text-muted)}.structure-toggle-btn .spinner{width:16px;height:16px;flex-shrink:0}.structure-toggle-arrow{margin-left:auto;font-size:.625rem;color:var(--color-text-muted);transition:transform .2s}.structure-panel{border:1px solid var(--color-primary);border-top:none;border-bottom-left-radius:var(--radius);border-bottom-right-radius:var(--radius);padding:.75rem;background:var(--color-bg);animation:panelSlideDown .15s ease-out}@keyframes panelSlideDown{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}
1
+ :root{--color-primary: #2563eb;--color-primary-hover: #1d4ed8;--color-success: #10b981;--color-error: #ef4444;--color-warning: #f59e0b;--color-bg: #ffffff;--color-bg-secondary: #f9fafb;--color-border: #e5e7eb;--color-text: #111827;--color-text-muted: #6b7280;--radius: 8px;--radius-sm: 4px;--shadow: 0 1px 3px rgba(0, 0, 0, .1);--shadow-lg: 0 4px 6px rgba(0, 0, 0, .1)}*{box-sizing:border-box;margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,sans-serif;background:var(--color-bg-secondary);color:var(--color-text);line-height:1.5}.app{min-height:100vh;display:flex;flex-direction:column}.header{background:var(--color-bg);border-bottom:1px solid var(--color-border);padding:1rem 2rem;display:flex;align-items:center;justify-content:space-between}.header h1{font-size:1.25rem;font-weight:600}.nav{display:flex;gap:1rem}.nav a{color:var(--color-text-muted);text-decoration:none;padding:.5rem 1rem;border-radius:var(--radius);transition:all .2s}.nav a:hover,.nav a.active{color:var(--color-primary);background:#2563eb1a}.main{flex:1;padding:2rem;max-width:1200px;margin:0 auto;width:100%}.card{background:var(--color-bg);border:1px solid var(--color-border);border-radius:var(--radius);padding:1.5rem;box-shadow:var(--shadow)}.card+.card{margin-top:1rem}.card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem}.card-title{font-size:1.125rem;font-weight:600}.btn{display:inline-flex;align-items:center;gap:.5rem;padding:.5rem 1rem;border-radius:var(--radius);border:none;font-size:.875rem;font-weight:500;cursor:pointer;transition:all .2s}.btn-primary{background:var(--color-primary);color:#fff}.btn-primary:hover{background:var(--color-primary-hover)}.btn-secondary{background:var(--color-bg);border:1px solid var(--color-border);color:var(--color-text)}.btn-secondary:hover{background:var(--color-bg-secondary)}.btn:disabled{opacity:.5;cursor:not-allowed}.dropzone{border:2px dashed var(--color-border);border-radius:var(--radius);padding:3rem;text-align:center;cursor:pointer;transition:all .2s}.dropzone:hover,.dropzone.active{border-color:var(--color-primary);background:#2563eb0d}.dropzone p{color:var(--color-text-muted);margin-top:.5rem}.file-list{margin-top:1rem}.file-item{display:flex;align-items:center;justify-content:space-between;padding:.75rem;border:1px solid var(--color-border);border-radius:var(--radius);margin-bottom:.5rem}.file-item .name{font-weight:500}.file-item .size{color:var(--color-text-muted);font-size:.875rem}.file-item .status{display:flex;align-items:center;gap:.5rem}.status-pending{color:var(--color-text-muted)}.status-uploading{color:var(--color-primary)}.status-success{color:var(--color-success)}.status-error{color:var(--color-error)}.document-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1rem}.document-card{background:var(--color-bg);border:1px solid var(--color-border);border-radius:var(--radius);padding:1rem;transition:all .2s}.document-card:hover{box-shadow:var(--shadow-lg)}.document-card .title{font-weight:600;margin-bottom:.25rem}.document-card .meta{color:var(--color-text-muted);font-size:.875rem;margin-bottom:.75rem}.document-card .actions{display:flex;gap:.5rem}.folder-tree{border:1px solid var(--color-border);border-radius:var(--radius);padding:1rem;max-height:400px;overflow-y:auto}.folder-item{padding:.5rem;cursor:pointer;border-radius:var(--radius);display:flex;align-items:center;gap:.5rem}.folder-item:hover{background:var(--color-bg-secondary)}.folder-item.selected{background:#2563eb1a;color:var(--color-primary)}.folder-item .icon{font-size:1.25rem}.modal-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1000}.modal{background:var(--color-bg);border-radius:var(--radius);padding:1.5rem;max-width:500px;width:90%;max-height:80vh;overflow-y:auto}.modal-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem}.modal-title{font-size:1.125rem;font-weight:600}.modal-close{background:none;border:none;font-size:1.5rem;cursor:pointer;color:var(--color-text-muted)}.form-group{margin-bottom:1rem}.form-label{display:block;font-weight:500;margin-bottom:.25rem}.form-input,.form-select{width:100%;padding:.5rem;border:1px solid var(--color-border);border-radius:var(--radius);font-size:1rem}.form-input:focus,.form-select:focus{outline:none;border-color:var(--color-primary)}.spinner{width:20px;height:20px;border:2px solid var(--color-border);border-top-color:var(--color-primary);border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.empty-state{text-align:center;padding:3rem;color:var(--color-text-muted)}.empty-state h3{color:var(--color-text);margin-bottom:.5rem}.alert{padding:1rem;border-radius:var(--radius);margin-bottom:1rem}.alert-success{background:#10b9811a;border:1px solid var(--color-success);color:var(--color-success)}.alert-error{background:#ef44441a;border:1px solid var(--color-error);color:var(--color-error)}.alert-warning{background:#f59e0b1a;border:1px solid var(--color-warning);color:var(--color-warning)}.breadcrumb{display:flex;align-items:center;flex-wrap:wrap;gap:0;font-size:.875rem}.breadcrumb-item{background:none;border:none;color:var(--color-primary);cursor:pointer;padding:.25rem .5rem;border-radius:var(--radius);font-size:inherit}.breadcrumb-item:hover{background:#2563eb1a}.breadcrumb-current{color:var(--color-text);cursor:default}.breadcrumb-current:hover{background:transparent}.breadcrumb-separator{color:var(--color-text-muted);margin:0 .25rem}.browse-page{display:flex;flex-direction:column;gap:1rem}.toolbar{display:flex;justify-content:space-between;align-items:center;background:var(--color-bg);border:1px solid var(--color-border);border-radius:var(--radius);padding:.75rem 1rem;gap:1rem;flex-wrap:wrap}.toolbar-left{display:flex;align-items:center;gap:.5rem;flex:1;min-width:200px}.toolbar-right{display:flex;align-items:center;gap:.5rem}.search-input{padding:.5rem .75rem;border:1px solid var(--color-border);border-radius:var(--radius);font-size:.875rem;width:200px}.search-input:focus{outline:none;border-color:var(--color-primary)}.content-area{background:var(--color-bg);border:1px solid var(--color-border);border-radius:var(--radius);overflow:hidden}.data-grid-container{display:flex;flex-direction:column}.data-grid{width:100%;border-collapse:collapse;font-size:.875rem}.data-grid thead{background:var(--color-bg-secondary);border-bottom:1px solid var(--color-border)}.data-grid th{text-align:left;padding:.75rem 1rem;font-weight:600;color:var(--color-text-muted);white-space:nowrap}.data-grid th.sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.data-grid th.sortable:hover{color:var(--color-text)}.data-grid td{padding:.75rem 1rem;border-bottom:1px solid var(--color-border);vertical-align:middle}.grid-row{transition:background .15s}.grid-row:hover{background:var(--color-bg-secondary)}.folder-row{cursor:pointer}.folder-row:hover{background:#2563eb0d}.col-checkbox{width:40px;text-align:center}.col-name{min-width:250px}.col-format,.col-type{width:120px}.col-updated,.col-created{width:140px}.col-actions{width:100px;text-align:right}.item-icon{margin-right:.5rem;font-size:1rem}.item-name{font-weight:500}.subfolder-count{color:var(--color-text-muted);font-size:.75rem;margin-left:.5rem}.btn-icon{background:none;border:none;padding:.25rem .5rem;cursor:pointer;font-size:1rem;opacity:.6;transition:opacity .15s}.btn-icon:hover{opacity:1}.btn-icon:disabled{opacity:.3;cursor:not-allowed}.btn-icon.pinned,.btn-delete:hover{opacity:1}.data-grid-footer{padding:.75rem 1rem;background:var(--color-bg-secondary);border-top:1px solid var(--color-border);font-size:.75rem;color:var(--color-text-muted)}.data-grid-loading,.data-grid-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:3rem;text-align:center;color:var(--color-text-muted);gap:.5rem}.data-grid-empty .empty-icon{font-size:3rem;margin-bottom:.5rem}.data-grid-empty h3{color:var(--color-text);margin:0}.data-grid-empty p{margin:.5rem 0 1rem}.folder-browser{border:1px solid var(--color-border);border-radius:var(--radius);overflow:hidden}.folder-browser-toolbar{display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;background:var(--color-bg-secondary);border-bottom:1px solid var(--color-border)}.folder-browser-actions{display:flex;gap:.5rem}.folder-browser-list{max-height:300px;overflow-y:auto}.folder-browser-table{width:100%;border-collapse:collapse}.folder-browser-row{transition:background .15s}.folder-browser-row:hover{background:var(--color-bg-secondary)}.folder-browser-row.selected{background:#2563eb1a}.folder-browser-row td{padding:.5rem;border-bottom:1px solid var(--color-border);vertical-align:middle}.folder-browser-icon{width:40px;text-align:center;font-size:1.25rem}.folder-browser-name{cursor:pointer;font-weight:500}.folder-browser-name:hover{color:var(--color-primary)}.subfolder-indicator{color:var(--color-text-muted);font-size:.75rem;font-weight:400;margin-left:.5rem}.folder-browser-select{width:100px;text-align:center}.folder-browser-delete{width:50px;text-align:center}.folder-browser-loading,.folder-browser-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem;color:var(--color-text-muted);gap:.75rem}.upload-page{display:flex;flex-direction:column;gap:1rem}.selected-folder-badge{background:#2563eb1a;color:var(--color-primary);padding:.25rem .75rem;border-radius:999px;font-size:.75rem;font-weight:500}.upload-actions{margin-top:1rem;display:flex;gap:.5rem;justify-content:flex-end}.btn-sm{padding:.25rem .5rem;font-size:.75rem}.document-row{cursor:pointer;transition:background .15s}.document-row:hover{background:var(--color-bg-secondary)}.document-row.expanded{background:#2563eb0d}.expand-icon{display:inline-block;width:1rem;margin-right:.25rem;transition:transform .2s;color:var(--color-text-muted)}.expand-icon.expanded{transform:rotate(90deg)}.expanded-row td{padding:0!important;background:var(--color-bg-secondary);border-bottom:2px solid var(--color-border)}.course-detail-panel{padding:1rem;display:flex;flex-direction:column;gap:1rem}.course-detail-loading,.course-detail-error{display:flex;align-items:center;justify-content:center;gap:.75rem;padding:2rem;color:var(--color-text-muted)}.course-detail-error{color:var(--color-error);flex-direction:column}.course-detail-tile{background:var(--color-bg);border:1px solid var(--color-border);border-radius:var(--radius);padding:1rem}.tile-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem;padding-bottom:.75rem;border-bottom:1px solid var(--color-border)}.tile-header h3{font-size:1rem;font-weight:600;margin:0}.btn-close{background:none;border:none;font-size:1.25rem;color:var(--color-text-muted);cursor:pointer;padding:.25rem;line-height:1;border-radius:var(--radius);transition:all .15s}.btn-close:hover{background:var(--color-bg-secondary);color:var(--color-text)}.detail-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:.75rem 1.5rem}.detail-item{display:flex;flex-direction:column;gap:.125rem}.detail-item.span-2{grid-column:span 2}.detail-label{font-size:.7rem;font-weight:600;color:var(--color-text-muted);text-transform:uppercase;letter-spacing:.025em}.detail-value{font-size:.875rem;color:var(--color-text)}.detail-value.mono{font-family:SF Mono,Monaco,Consolas,monospace;font-size:.75rem;background:var(--color-bg-secondary);padding:.125rem .375rem;border-radius:4px}.detail-value.description{line-height:1.4;max-height:4.2em;overflow:hidden;text-overflow:ellipsis}.thin-pack-row{display:grid;grid-template-columns:repeat(2,1fr);gap:1rem}.thin-pack-tile{background:var(--color-bg);border:1px solid var(--color-border);border-radius:var(--radius);padding:1rem;border-left:4px solid}.thin-pack-tile.public{border-left-color:var(--color-success)}.thin-pack-tile.private{border-left-color:var(--color-primary)}.thin-pack-header{display:flex;align-items:flex-start;gap:.75rem;margin-bottom:.75rem}.thin-pack-icon{font-size:1.5rem}.thin-pack-titles{flex:1}.thin-pack-title{font-size:.9rem;font-weight:600;margin:0}.thin-pack-subtitle{font-size:.75rem;color:var(--color-text-muted);margin:.125rem 0 0}.thin-pack-content{display:flex;flex-direction:column;gap:.5rem}.thin-pack-error{font-size:.75rem;color:var(--color-error);padding:.5rem;background:#ef44441a;border-radius:4px}.thin-pack-status{display:flex;align-items:center;gap:.5rem;font-size:.8rem}.thin-pack-status.exists{color:var(--color-success)}.thin-pack-status.exists .status-icon{display:inline-flex;align-items:center;justify-content:center;width:1.25rem;height:1.25rem;background:var(--color-success);color:#fff;border-radius:50%;font-size:.7rem}.thin-pack-status.missing{color:var(--color-text-muted);font-style:italic}.thin-pack-meta{display:flex;gap:1rem;font-size:.7rem;color:var(--color-text-muted)}.thin-pack-actions{display:flex;gap:.5rem;margin-top:.25rem}.thin-pack-slug-input{display:flex;flex-direction:column;gap:.25rem}.thin-pack-slug-input label{font-size:.7rem;color:var(--color-text-muted)}.thin-pack-slug-input input{padding:.375rem .5rem;font-size:.8rem;border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-bg);color:var(--color-text)}.thin-pack-slug-input input:focus{outline:none;border-color:var(--color-primary)}.thin-pack-slug-input input::placeholder{color:var(--color-text-muted);opacity:.6}.thin-pack-name-input{display:flex;flex-direction:column;gap:.25rem}.thin-pack-name-input label{font-size:.7rem;color:var(--color-text-muted)}.thin-pack-name-input input{padding:.375rem .5rem;font-size:.8rem;border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-bg);color:var(--color-text)}.thin-pack-name-input input:focus{outline:none;border-color:var(--color-primary)}.thin-pack-name-input input::placeholder{color:var(--color-text-muted);opacity:.6}.token-preview{font-size:.65rem;color:var(--color-text-muted);font-family:monospace;word-break:break-all}.thin-pack-visibility{display:flex;flex-direction:column;gap:.25rem;padding:.5rem;background:var(--color-bg-tertiary);border-radius:4px}.visibility-toggle{display:flex;align-items:center;gap:.5rem;cursor:pointer}.visibility-toggle input[type=checkbox]{width:16px;height:16px;cursor:pointer}.toggle-label{font-size:.85rem;font-weight:500}.visibility-hint{font-size:.7rem;color:var(--color-text-muted);margin-left:24px}.thin-pack-tile.lms{border-left-color:var(--color-success)}.thin-pack-tile.shared{border-left-color:var(--color-primary)}.upload-modal{width:600px;max-width:90vw;max-height:90vh;display:flex;flex-direction:column}.upload-modal-content{flex:1;overflow-y:auto;padding:1rem;display:flex;flex-direction:column;gap:1rem}.upload-modal .dropzone{min-height:120px}.upload-file-list{display:flex;flex-direction:column;gap:.5rem}.upload-file-list h4{margin:0;font-size:.85rem;color:var(--color-text-muted)}.upload-file-item{display:flex;justify-content:space-between;align-items:center;padding:.5rem;background:var(--color-bg-secondary);border-radius:8px}.upload-file-info{display:flex;flex-direction:column;gap:.125rem}.upload-file-name{font-weight:500;font-size:.875rem}.upload-file-size{font-size:.75rem;color:var(--color-text-muted)}.upload-file-status{display:flex;align-items:center;gap:.5rem}.upload-progress-container{display:flex;align-items:center;gap:.75rem;min-width:150px}.upload-progress-bar{flex:1;height:8px;background:var(--color-border);border-radius:4px;overflow:hidden}.upload-progress-fill{height:100%;background:var(--color-primary);transition:width .15s ease-out}.upload-progress-text{font-size:.75rem;font-weight:500;color:var(--color-text-muted);min-width:36px;text-align:right}.status-processing{color:var(--color-primary);font-size:.875rem}.upload-link-options{background:var(--color-bg-secondary);border-radius:var(--radius);padding:1rem;display:flex;flex-direction:column;gap:.75rem}.upload-link-options h4{margin:0;font-size:.85rem;color:var(--color-text-muted)}.upload-link-option{display:flex;flex-direction:column;gap:.5rem}.upload-link-toggle{display:flex;align-items:center;gap:.5rem;cursor:pointer}.upload-link-toggle input[type=checkbox]{width:1rem;height:1rem;cursor:pointer}.upload-link-toggle span{font-size:.875rem}.upload-link-name-input{display:flex;flex-direction:column;gap:.25rem;padding-left:1.5rem}.upload-link-name-input input{padding:.375rem .5rem;font-size:.8rem;border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-bg);color:var(--color-text)}.upload-link-name-input input:focus{outline:none;border-color:var(--color-primary)}.upload-link-name-input input::placeholder{color:var(--color-text-muted);opacity:.6}.slug-preview{font-size:.7rem;color:var(--color-text-muted);font-family:monospace}.modal-footer{display:flex;justify-content:flex-end;gap:.5rem;padding:1rem;border-top:1px solid var(--color-border)}@media (max-width: 768px){.detail-grid{grid-template-columns:1fr}.detail-item.span-2{grid-column:span 1}.thin-pack-row{grid-template-columns:1fr}}.user-menu{display:flex;align-items:center;gap:.75rem;margin-left:auto}.user-info{display:flex;align-items:center;gap:.5rem}.user-avatar{width:2rem;height:2rem;border-radius:50%;background:var(--color-primary);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:600;font-size:.875rem}.user-name{font-size:.875rem;color:var(--color-text);max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.header{display:flex;align-items:center;gap:1.5rem}.header .nav{margin-right:auto}.course-structure-step{display:flex;flex-direction:column;gap:.5rem}.course-structure-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:.25rem}.course-structure-header h4{margin:0;font-size:1rem;font-weight:600}.course-structure-meta{display:flex;align-items:center;gap:.5rem;font-size:.75rem;color:var(--color-text-muted)}.course-structure-badge{background:#2563eb1a;color:var(--color-primary);padding:.125rem .5rem;border-radius:999px;font-size:.7rem;font-weight:500}.tree-node{padding:.25rem 0}.tree-node-row{display:flex;align-items:center;gap:.375rem;min-height:1.5rem}.tree-node-icon{font-size:1rem;flex-shrink:0}.tree-node-label{font-weight:500;font-size:.875rem;flex:1}.tree-class-input{width:100%;padding:.25rem .5rem;font-size:.8rem;border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-bg);color:var(--color-text);margin-top:.25rem}.tree-class-input:focus{outline:none;border-color:var(--color-primary)}.tree-class-input::placeholder{color:var(--color-text-muted);opacity:.6}.tree-class-input-sm{max-width:200px;margin-top:0;margin-left:auto;flex-shrink:0}.tree-course{background:var(--color-bg-secondary);border:1px solid var(--color-border);border-radius:var(--radius);padding:.5rem .75rem}.tree-lessons{display:flex;flex-direction:column;gap:.125rem;max-height:400px;overflow-y:auto;border:1px solid var(--color-border);border-radius:var(--radius);padding:.25rem}.tree-lesson{border-bottom:1px solid var(--color-border)}.tree-lesson:last-child{border-bottom:none}.tree-node-lesson{cursor:pointer;padding:.375rem .5rem;border-radius:var(--radius-sm);transition:background .15s}.tree-node-lesson:hover{background:var(--color-bg-secondary)}.tree-expand{font-size:.625rem;width:1rem;text-align:center;color:var(--color-text-muted);flex-shrink:0}.tree-lesson-num{color:var(--color-text-muted);font-weight:600;font-size:.75rem}.tree-node-indicator{font-size:.65rem;color:var(--color-primary);background:#2563eb1a;padding:.0625rem .375rem;border-radius:999px;white-space:nowrap;flex-shrink:0}.tree-lesson-content{padding:.25rem .5rem .5rem 1.5rem}.tree-lesson-class{margin-bottom:.375rem}.tree-node-block{display:flex;align-items:center;gap:.375rem;padding:.1875rem 0}.tree-node-block .tree-node-row{flex:1;min-width:0}.tree-block-pipe{font-family:monospace;color:var(--color-border);font-size:.75rem;flex-shrink:0}.tree-block-type{font-size:.75rem;font-weight:500;color:var(--color-text);white-space:nowrap}.tree-block-title{font-size:.7rem;color:var(--color-text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:200px}.structure-parsing-indicator{display:flex;align-items:center;gap:.75rem;padding:.75rem;color:var(--color-primary);font-size:.875rem}.structure-detected-badge{background:#10b9811a;color:var(--color-success);border:1px solid var(--color-success);border-radius:var(--radius);padding:.5rem .75rem;font-size:.8rem}.structure-multi-note{font-size:.75rem;color:var(--color-text-muted);font-style:italic;padding:.25rem 0}.structure-summary-badge{background:#2563eb1a;color:var(--color-primary);border-radius:var(--radius);padding:.5rem .75rem;font-size:.8rem;margin-top:.25rem}.structure-section{display:flex;flex-direction:column}.structure-toggle-btn{display:flex;align-items:center;gap:.5rem;width:100%;padding:.625rem .75rem;background:var(--color-bg);border:1px solid var(--color-border);border-radius:var(--radius);cursor:pointer;font-size:.875rem;font-weight:500;color:var(--color-text);transition:all .2s}.structure-toggle-btn:hover:not(:disabled){border-color:var(--color-primary);background:#2563eb08}.structure-toggle-btn.open{border-color:var(--color-primary);border-bottom-left-radius:0;border-bottom-right-radius:0;background:#2563eb0d}.structure-toggle-btn.disabled{opacity:.5;cursor:not-allowed;color:var(--color-text-muted)}.structure-toggle-btn .spinner{width:16px;height:16px;flex-shrink:0}.structure-toggle-arrow{margin-left:auto;font-size:.625rem;color:var(--color-text-muted);transition:transform .2s}.structure-panel{border:1px solid var(--color-primary);border-top:none;border-bottom-left-radius:var(--radius);border-bottom-right-radius:var(--radius);padding:.75rem;background:var(--color-bg);animation:panelSlideDown .15s ease-out}@keyframes panelSlideDown{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.block-cards{display:flex;flex-direction:column;gap:3px;padding:.375rem 0}.block-card{display:flex;flex-direction:column;gap:.25rem;padding:.5rem .625rem;border-radius:var(--radius-sm);border:1px solid var(--color-border);border-left:4px solid var(--block-accent, #6b7280);background:var(--color-bg);cursor:pointer;transition:all .15s}.block-card:hover:not(.grouped){border-color:var(--color-primary);border-left-color:var(--block-accent, #6b7280);background:#2563eb08}.block-card.selected{background:#2563eb14;border-color:var(--color-primary);border-left-color:var(--block-accent, #6b7280);box-shadow:0 0 0 1px #2563eb33}.block-card.grouped{cursor:default;opacity:.75;background:var(--color-bg-secondary)}.block-card-header{display:flex;align-items:center;gap:.5rem;min-height:1.25rem}.block-card-type{font-size:.8rem;font-weight:600;color:var(--color-text);white-space:nowrap}.block-card-title{font-size:.75rem;color:var(--color-text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}.block-card-check{color:var(--color-primary);font-weight:700;font-size:.875rem;flex-shrink:0;margin-left:auto}.block-card .tree-class-input-sm{margin-left:0;max-width:100%;margin-top:0}.block-group-container{border:2px dashed var(--color-primary);border-radius:var(--radius);padding:.375rem;margin:.25rem 0;background:#2563eb05}.block-group-container .block-card{border-left-width:3px}.bgp-group-header{display:flex;align-items:center;gap:.5rem;padding:.25rem .5rem;font-size:.7rem;border-bottom:1px solid rgba(37,99,235,.15);margin-bottom:.25rem}.bgp-group-label{font-weight:600;color:var(--color-primary)}.bgp-group-class{color:var(--color-text-muted);font-family:monospace;font-size:.65rem}.bgp-btn{background:none;border:none;font-size:.65rem;cursor:pointer;padding:.125rem .375rem;border-radius:var(--radius-sm);transition:all .15s}.bgp-btn-edit{color:var(--color-text-muted);margin-left:auto}.bgp-btn-edit:hover{color:var(--color-primary);background:#2563eb1a}.bgp-btn-ungroup{color:var(--color-text-muted)}.bgp-btn-ungroup:hover{color:var(--color-error);background:#ef44441a}.bgp-btn-save{color:var(--color-success)}.bgp-btn-save:hover{background:#10b9811a}.bgp-btn-cancel{color:var(--color-text-muted)}.bgp-btn-cancel:hover{background:var(--color-bg-secondary)}.bgp-group-edit{display:flex;align-items:center;gap:.375rem;flex:1}.bgp-edit-input{padding:.1875rem .375rem;font-size:.7rem;border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-bg);color:var(--color-text);flex:1}.bgp-edit-input:focus{outline:none;border-color:var(--color-primary)}.bgp-edit-input-sm{max-width:120px}.bgp-create{padding:.5rem .75rem;margin-top:.375rem;border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-bg-secondary);display:flex;flex-direction:column;gap:.375rem}.bgp-create-info{font-size:.75rem;font-weight:500;color:var(--color-text)}.bgp-validation-error{color:var(--color-error);font-weight:400}.bgp-create-inputs{display:flex;gap:.375rem}.bgp-create-class{flex:2;padding:.25rem .5rem;font-size:.8rem;border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-bg);color:var(--color-text)}.bgp-create-class:focus{outline:none;border-color:var(--color-primary)}.bgp-create-label{flex:1;padding:.25rem .5rem;font-size:.8rem;border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-bg);color:var(--color-text)}.bgp-create-label:focus{outline:none;border-color:var(--color-primary)}.bgp-create-actions{display:flex;gap:.375rem}.bgp-hint{padding:.75rem;font-size:.75rem;color:var(--color-text-muted);text-align:center;font-style:italic}
package/dist/index.html CHANGED
@@ -5,8 +5,8 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>Package Uploader</title>
8
- <script type="module" crossorigin src="/assets/index-Ca5beg0c.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-C19M6liw.css">
8
+ <script type="module" crossorigin src="/assets/index-BZdhKDnY.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-BfowR104.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@package-uploader/ui",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "React UI for uploading and browsing courses on LMS platforms",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/api/client.ts CHANGED
@@ -120,10 +120,19 @@ export interface AppConfig {
120
120
  user: UserProfile | null;
121
121
  }
122
122
 
123
+ export interface BlockGroup {
124
+ id: string;
125
+ lessonId: string;
126
+ blockIds: string[];
127
+ className: string;
128
+ label?: string;
129
+ }
130
+
123
131
  export interface CourseClassMappings {
124
132
  course?: string;
125
133
  lessons?: Record<string, string>;
126
134
  blocks?: Record<string, string>;
135
+ blockGroups?: BlockGroup[];
127
136
  }
128
137
 
129
138
  async function request<T>(
@@ -1,4 +1,5 @@
1
1
  import { useState } from 'react';
2
+ import { useBlockGrouping, type BlockGroup } from '../hooks/useBlockGrouping';
2
3
 
3
4
  /** Block within a lesson */
4
5
  interface CourseStructureBlock {
@@ -24,11 +25,12 @@ export interface CourseStructure {
24
25
  lessons: CourseStructureLesson[];
25
26
  }
26
27
 
27
- /** Class assignments at course/lesson/block levels */
28
+ /** Class assignments at course/lesson/block/group levels */
28
29
  export interface CourseClassMappings {
29
30
  course?: string;
30
31
  lessons?: Record<string, string>;
31
32
  blocks?: Record<string, string>;
33
+ blockGroups?: BlockGroup[];
32
34
  }
33
35
 
34
36
  interface CourseStructureStepProps {
@@ -37,25 +39,301 @@ interface CourseStructureStepProps {
37
39
  onChange: (mappings: CourseClassMappings) => void;
38
40
  }
39
41
 
42
+ /** Color palette for block families */
43
+ function familyColor(family?: string): string {
44
+ switch (family) {
45
+ case 'text': return '#3b82f6';
46
+ case 'media': return '#22c55e';
47
+ case 'interactive': return '#a855f7';
48
+ case 'knowledge-check': return '#f59e0b';
49
+ case 'quote': return '#ec4899';
50
+ default: return '#6b7280';
51
+ }
52
+ }
53
+
54
+ function formatBlockLabel(block: CourseStructureBlock): string {
55
+ const parts = [block.type];
56
+ if (block.variant) parts.push(block.variant);
57
+ return parts.join(' / ');
58
+ }
59
+
60
+ // --- LessonNode: extracted so useBlockGrouping can be called per lesson ---
61
+
62
+ interface LessonNodeProps {
63
+ lesson: CourseStructureLesson;
64
+ lessonIdx: number;
65
+ classMappings: CourseClassMappings;
66
+ onSetLessonClass: (lessonId: string, value: string) => void;
67
+ onSetBlockClass: (blockId: string, value: string) => void;
68
+ onGroupsChange: (lessonId: string, groups: BlockGroup[]) => void;
69
+ }
70
+
71
+ function LessonNode({
72
+ lesson,
73
+ lessonIdx,
74
+ classMappings,
75
+ onSetLessonClass,
76
+ onSetBlockClass,
77
+ onGroupsChange,
78
+ }: LessonNodeProps) {
79
+ const [isExpanded, setIsExpanded] = useState(false);
80
+
81
+ // Group edit state (absorbed from BlockGroupingPanel)
82
+ const [newGroupClass, setNewGroupClass] = useState('');
83
+ const [newGroupLabel, setNewGroupLabel] = useState('');
84
+ const [editingGroupId, setEditingGroupId] = useState<string | null>(null);
85
+ const [editClass, setEditClass] = useState('');
86
+ const [editLabel, setEditLabel] = useState('');
87
+
88
+ const lessonGroups = (classMappings.blockGroups || []).filter(
89
+ (g) => g.lessonId === lesson.id
90
+ );
91
+
92
+ const lessonBlockIds = lesson.blocks.map((b) => b.id);
93
+
94
+ const {
95
+ selectedBlockIds,
96
+ toggleBlock,
97
+ clearSelection,
98
+ canCreateGroup,
99
+ validationError,
100
+ createGroup,
101
+ removeGroup,
102
+ updateGroup,
103
+ isBlockGrouped,
104
+ } = useBlockGrouping({
105
+ lessonBlockIds,
106
+ lessonId: lesson.id,
107
+ groups: lessonGroups,
108
+ onGroupsChange: (groups) => onGroupsChange(lesson.id, groups),
109
+ });
110
+
111
+ const lessonHasClass = !!classMappings.lessons?.[lesson.id];
112
+ const blocksWithClass = lesson.blocks.filter(
113
+ (b) => !!classMappings.blocks?.[b.id]
114
+ ).length;
115
+
116
+ // --- Group edit handlers ---
117
+ const handleCreateGroup = () => {
118
+ if (!canCreateGroup || !newGroupClass.trim()) return;
119
+ createGroup(newGroupClass, newGroupLabel || undefined);
120
+ setNewGroupClass('');
121
+ setNewGroupLabel('');
122
+ };
123
+
124
+ const startEditGroup = (group: BlockGroup) => {
125
+ setEditingGroupId(group.id);
126
+ setEditClass(group.className);
127
+ setEditLabel(group.label || '');
128
+ };
129
+
130
+ const saveEditGroup = () => {
131
+ if (editingGroupId) {
132
+ updateGroup(editingGroupId, { className: editClass, label: editLabel });
133
+ setEditingGroupId(null);
134
+ }
135
+ };
136
+
137
+ // --- Block card rendering ---
138
+ const renderBlockCard = (block: CourseStructureBlock, isGrouped: boolean) => {
139
+ const isSelected = selectedBlockIds.has(block.id);
140
+ return (
141
+ <div
142
+ key={block.id}
143
+ className={`block-card${isSelected ? ' selected' : ''}${isGrouped ? ' grouped' : ''}`}
144
+ onClick={() => !isGrouped && toggleBlock(block.id)}
145
+ style={{ '--block-accent': familyColor(block.family) } as React.CSSProperties}
146
+ title={isGrouped ? 'Already in a group' : 'Click to select for grouping'}
147
+ >
148
+ <div className="block-card-header">
149
+ <span className="block-card-type">{formatBlockLabel(block)}</span>
150
+ {block.title && (
151
+ <span className="block-card-title" title={block.title}>
152
+ {block.title.length > 40 ? block.title.substring(0, 40) + '...' : block.title}
153
+ </span>
154
+ )}
155
+ {isSelected && <span className="block-card-check">&#10003;</span>}
156
+ </div>
157
+ <input
158
+ type="text"
159
+ className="tree-class-input tree-class-input-sm"
160
+ placeholder="class..."
161
+ value={classMappings.blocks?.[block.id] || ''}
162
+ onChange={(e) => onSetBlockClass(block.id, e.target.value)}
163
+ onClick={(e) => e.stopPropagation()}
164
+ />
165
+ </div>
166
+ );
167
+ };
168
+
169
+ // --- Group header rendering ---
170
+ const renderGroupHeader = (group: BlockGroup) => {
171
+ if (editingGroupId === group.id) {
172
+ return (
173
+ <div className="bgp-group-header">
174
+ <div className="bgp-group-edit">
175
+ <input
176
+ type="text"
177
+ value={editClass}
178
+ onChange={(e) => setEditClass(e.target.value)}
179
+ placeholder="CSS class(es)"
180
+ className="bgp-edit-input"
181
+ />
182
+ <input
183
+ type="text"
184
+ value={editLabel}
185
+ onChange={(e) => setEditLabel(e.target.value)}
186
+ placeholder="Label (optional)"
187
+ className="bgp-edit-input bgp-edit-input-sm"
188
+ />
189
+ <button className="bgp-btn bgp-btn-save" onClick={saveEditGroup}>Save</button>
190
+ <button className="bgp-btn bgp-btn-cancel" onClick={() => setEditingGroupId(null)}>Cancel</button>
191
+ </div>
192
+ </div>
193
+ );
194
+ }
195
+
196
+ return (
197
+ <div className="bgp-group-header">
198
+ <span className="bgp-group-label">{group.label || group.className}</span>
199
+ <span className="bgp-group-class">.{group.className}</span>
200
+ <button className="bgp-btn bgp-btn-edit" onClick={() => startEditGroup(group)} title="Edit group">Edit</button>
201
+ <button className="bgp-btn bgp-btn-ungroup" onClick={() => removeGroup(group.id)} title="Remove group">Ungroup</button>
202
+ </div>
203
+ );
204
+ };
205
+
206
+ // --- Build block card list with inline group containers ---
207
+ const blockGroupMap = new Map<string, BlockGroup>();
208
+ for (const group of lessonGroups) {
209
+ for (const bid of group.blockIds) {
210
+ blockGroupMap.set(bid, group);
211
+ }
212
+ }
213
+
214
+ const renderedGroups = new Set<string>();
215
+ const blockItems: React.ReactNode[] = [];
216
+
217
+ for (const block of lesson.blocks) {
218
+ const group = blockGroupMap.get(block.id);
219
+ if (group) {
220
+ if (renderedGroups.has(group.id)) continue;
221
+ renderedGroups.add(group.id);
222
+ const groupBlocks = group.blockIds
223
+ .map((bid) => lesson.blocks.find((b) => b.id === bid))
224
+ .filter(Boolean) as CourseStructureBlock[];
225
+ blockItems.push(
226
+ <div key={`group-${group.id}`} className="block-group-container">
227
+ {renderGroupHeader(group)}
228
+ {groupBlocks.map((b) => renderBlockCard(b, true))}
229
+ </div>
230
+ );
231
+ } else {
232
+ blockItems.push(renderBlockCard(block, false));
233
+ }
234
+ }
235
+
236
+ const hasSelection = selectedBlockIds.size > 0;
237
+
238
+ return (
239
+ <div className="tree-lesson">
240
+ <div
241
+ className="tree-node tree-node-lesson"
242
+ onClick={() => setIsExpanded((prev) => !prev)}
243
+ >
244
+ <div className="tree-node-row">
245
+ <span className="tree-expand">{isExpanded ? '\u25BC' : '\u25B6'}</span>
246
+ <span className="tree-node-label">
247
+ <span className="tree-lesson-num">{lessonIdx + 1}.</span>
248
+ {' '}{lesson.title}
249
+ </span>
250
+ {(lessonHasClass || blocksWithClass > 0 || lessonGroups.length > 0) && !isExpanded && (
251
+ <span className="tree-node-indicator">
252
+ {[
253
+ lessonHasClass ? 'lesson' : '',
254
+ blocksWithClass > 0 ? `${blocksWithClass} block${blocksWithClass > 1 ? 's' : ''}` : '',
255
+ lessonGroups.length > 0 ? `${lessonGroups.length} group${lessonGroups.length > 1 ? 's' : ''}` : '',
256
+ ].filter(Boolean).join(' + ')}
257
+ </span>
258
+ )}
259
+ </div>
260
+ </div>
261
+
262
+ {isExpanded && (
263
+ <div className="tree-lesson-content">
264
+ <div className="tree-lesson-class">
265
+ <input
266
+ type="text"
267
+ className="tree-class-input"
268
+ placeholder="Lesson-level class..."
269
+ value={classMappings.lessons?.[lesson.id] || ''}
270
+ onChange={(e) => onSetLessonClass(lesson.id, e.target.value)}
271
+ onClick={(e) => e.stopPropagation()}
272
+ />
273
+ </div>
274
+
275
+ {/* Visual block cards */}
276
+ <div className="block-cards">
277
+ {blockItems}
278
+ </div>
279
+
280
+ {/* Create group controls */}
281
+ {hasSelection && (
282
+ <div className="bgp-create">
283
+ <div className="bgp-create-info">
284
+ {selectedBlockIds.size} block{selectedBlockIds.size !== 1 ? 's' : ''} selected
285
+ {validationError && <span className="bgp-validation-error"> — {validationError}</span>}
286
+ </div>
287
+ <div className="bgp-create-inputs">
288
+ <input
289
+ type="text"
290
+ value={newGroupClass}
291
+ onChange={(e) => setNewGroupClass(e.target.value)}
292
+ placeholder="CSS class(es) for wrapper..."
293
+ className="bgp-create-class"
294
+ onKeyDown={(e) => e.key === 'Enter' && handleCreateGroup()}
295
+ />
296
+ <input
297
+ type="text"
298
+ value={newGroupLabel}
299
+ onChange={(e) => setNewGroupLabel(e.target.value)}
300
+ placeholder="Label (optional)"
301
+ className="bgp-create-label"
302
+ />
303
+ </div>
304
+ <div className="bgp-create-actions">
305
+ <button
306
+ className="btn btn-sm btn-primary"
307
+ onClick={handleCreateGroup}
308
+ disabled={!canCreateGroup || !newGroupClass.trim()}
309
+ >
310
+ Create Group
311
+ </button>
312
+ <button className="btn btn-sm btn-secondary" onClick={clearSelection}>
313
+ Cancel
314
+ </button>
315
+ </div>
316
+ </div>
317
+ )}
318
+
319
+ {!hasSelection && lesson.blocks.length >= 2 && lessonGroups.length === 0 && (
320
+ <div className="bgp-hint">
321
+ Click adjacent blocks to select them, then create a group.
322
+ </div>
323
+ )}
324
+ </div>
325
+ )}
326
+ </div>
327
+ );
328
+ }
329
+
330
+ // --- Main component ---
331
+
40
332
  export default function CourseStructureStep({
41
333
  structure,
42
334
  classMappings,
43
335
  onChange,
44
336
  }: CourseStructureStepProps) {
45
- const [expandedLessons, setExpandedLessons] = useState<Set<string>>(new Set());
46
-
47
- const toggleLesson = (lessonId: string) => {
48
- setExpandedLessons((prev) => {
49
- const next = new Set(prev);
50
- if (next.has(lessonId)) {
51
- next.delete(lessonId);
52
- } else {
53
- next.add(lessonId);
54
- }
55
- return next;
56
- });
57
- };
58
-
59
337
  const setCourseClass = (value: string) => {
60
338
  onChange({ ...classMappings, course: value || undefined });
61
339
  };
@@ -80,24 +358,29 @@ export default function CourseStructureStep({
80
358
  onChange({ ...classMappings, blocks: Object.keys(blocks).length > 0 ? blocks : undefined });
81
359
  };
82
360
 
361
+ const handleGroupsChange = (lessonId: string, lessonGroups: BlockGroup[]) => {
362
+ const otherGroups = (classMappings.blockGroups || []).filter((g) => g.lessonId !== lessonId);
363
+ const allGroups = [...otherGroups, ...lessonGroups];
364
+ onChange({
365
+ ...classMappings,
366
+ blockGroups: allGroups.length > 0 ? allGroups : undefined,
367
+ });
368
+ };
369
+
83
370
  const clearAll = () => {
84
371
  onChange({});
85
372
  };
86
373
 
87
374
  // Count total assignments
375
+ const groupCount = (classMappings.blockGroups || []).length;
88
376
  const assignmentCount =
89
377
  (classMappings.course ? 1 : 0) +
90
378
  Object.keys(classMappings.lessons || {}).length +
91
- Object.keys(classMappings.blocks || {}).length;
379
+ Object.keys(classMappings.blocks || {}).length +
380
+ groupCount;
92
381
 
93
382
  const blockCount = structure.lessons.reduce((sum, l) => sum + l.blocks.length, 0);
94
383
 
95
- const formatBlockLabel = (block: CourseStructureBlock): string => {
96
- const parts = [block.type];
97
- if (block.variant) parts.push(block.variant);
98
- return parts.join(' / ');
99
- };
100
-
101
384
  return (
102
385
  <div className="course-structure-step">
103
386
  <div className="course-structure-header">
@@ -134,77 +417,17 @@ export default function CourseStructureStep({
134
417
 
135
418
  {/* Lessons */}
136
419
  <div className="tree-lessons">
137
- {structure.lessons.map((lesson, lessonIdx) => {
138
- const isExpanded = expandedLessons.has(lesson.id);
139
- const lessonHasClass = !!classMappings.lessons?.[lesson.id];
140
- const blocksWithClass = lesson.blocks.filter(
141
- (b) => !!classMappings.blocks?.[b.id]
142
- ).length;
143
-
144
- return (
145
- <div key={lesson.id} className="tree-lesson">
146
- <div
147
- className="tree-node tree-node-lesson"
148
- onClick={() => toggleLesson(lesson.id)}
149
- >
150
- <div className="tree-node-row">
151
- <span className="tree-expand">{isExpanded ? '\u25BC' : '\u25B6'}</span>
152
- <span className="tree-node-label">
153
- <span className="tree-lesson-num">{lessonIdx + 1}.</span>
154
- {' '}{lesson.title}
155
- </span>
156
- {(lessonHasClass || blocksWithClass > 0) && !isExpanded && (
157
- <span className="tree-node-indicator">
158
- {lessonHasClass && blocksWithClass > 0
159
- ? `lesson + ${blocksWithClass} block${blocksWithClass > 1 ? 's' : ''}`
160
- : lessonHasClass
161
- ? 'lesson'
162
- : `${blocksWithClass} block${blocksWithClass > 1 ? 's' : ''}`}
163
- </span>
164
- )}
165
- </div>
166
- </div>
167
-
168
- {isExpanded && (
169
- <div className="tree-lesson-content">
170
- <div className="tree-lesson-class">
171
- <input
172
- type="text"
173
- className="tree-class-input"
174
- placeholder="Lesson-level class..."
175
- value={classMappings.lessons?.[lesson.id] || ''}
176
- onChange={(e) => setLessonClass(lesson.id, e.target.value)}
177
- onClick={(e) => e.stopPropagation()}
178
- />
179
- </div>
180
-
181
- {lesson.blocks.map((block) => (
182
- <div key={block.id} className="tree-node tree-node-block">
183
- <div className="tree-node-row">
184
- <span className="tree-block-pipe">|--</span>
185
- <span className="tree-block-type">{formatBlockLabel(block)}</span>
186
- {block.title && (
187
- <span className="tree-block-title" title={block.title}>
188
- {block.title.length > 40
189
- ? block.title.substring(0, 40) + '...'
190
- : block.title}
191
- </span>
192
- )}
193
- </div>
194
- <input
195
- type="text"
196
- className="tree-class-input tree-class-input-sm"
197
- placeholder="class..."
198
- value={classMappings.blocks?.[block.id] || ''}
199
- onChange={(e) => setBlockClass(block.id, e.target.value)}
200
- />
201
- </div>
202
- ))}
203
- </div>
204
- )}
205
- </div>
206
- );
207
- })}
420
+ {structure.lessons.map((lesson, lessonIdx) => (
421
+ <LessonNode
422
+ key={lesson.id}
423
+ lesson={lesson}
424
+ lessonIdx={lessonIdx}
425
+ classMappings={classMappings}
426
+ onSetLessonClass={setLessonClass}
427
+ onSetBlockClass={setBlockClass}
428
+ onGroupsChange={handleGroupsChange}
429
+ />
430
+ ))}
208
431
  </div>
209
432
  </div>
210
433
  );
@@ -123,7 +123,8 @@ export default function UploadModal({
123
123
  const hasClassAssignments =
124
124
  classMappings.course ||
125
125
  Object.keys(classMappings.lessons || {}).length > 0 ||
126
- Object.keys(classMappings.blocks || {}).length > 0;
126
+ Object.keys(classMappings.blocks || {}).length > 0 ||
127
+ (classMappings.blockGroups && classMappings.blockGroups.length > 0);
127
128
 
128
129
  for (let i = 0; i < files.length; i++) {
129
130
  if (files[i].status !== 'pending') continue;
@@ -236,10 +237,12 @@ export default function UploadModal({
236
237
  : '(will use document name)';
237
238
 
238
239
  // Class assignment count for summary
240
+ const groupCount = (classMappings.blockGroups || []).length;
239
241
  const assignmentCount =
240
242
  (classMappings.course ? 1 : 0) +
241
243
  Object.keys(classMappings.lessons || {}).length +
242
- Object.keys(classMappings.blocks || {}).length;
244
+ Object.keys(classMappings.blocks || {}).length +
245
+ groupCount;
243
246
 
244
247
  // Customize button label
245
248
  const customizeLabel = parsingStructure
@@ -440,6 +443,9 @@ export default function UploadModal({
440
443
  Object.keys(classMappings.blocks || {}).length > 0
441
444
  ? `${Object.keys(classMappings.blocks || {}).length} block(s)`
442
445
  : '',
446
+ groupCount > 0
447
+ ? `${groupCount} group(s)`
448
+ : '',
443
449
  ]
444
450
  .filter(Boolean)
445
451
  .join(', ')}