Sorry, the diff of this file is too big to display
| /* =========================== | ||
| CSS CUSTOM PROPERTIES (Theming) | ||
| Override these variables to create custom themes. | ||
| =========================== */ | ||
| /* Default: Light theme */ | ||
| .beblob-widget { | ||
| --beblob-bg: #e8e6e3; | ||
| --beblob-text: #2c2c2c; | ||
| --beblob-text-muted: #777; | ||
| --beblob-accent: #d4cfc9; | ||
| --beblob-border: #d0cec9; | ||
| --beblob-link: #5a7de0; | ||
| --beblob-comment-bg: #f0eeed; | ||
| --beblob-comment-border: #d8d5d0; | ||
| --beblob-button-bg: transparent; | ||
| --beblob-button-text: #2c2c2c; | ||
| --beblob-button-border: #c4c1bc; | ||
| --beblob-button-hover-bg: rgba(0, 0, 0, 0.06); | ||
| --beblob-tab-bg: transparent; | ||
| --beblob-tab-active-bg: #d4cfc9; | ||
| --beblob-tab-active-text: #2c2c2c; | ||
| --beblob-reaction-bg: transparent; | ||
| --beblob-reaction-border: #c4c1bc; | ||
| --beblob-reaction-hover-bg: rgba(0, 0, 0, 0.06); | ||
| --beblob-popup-bg: #ddd9d4; | ||
| --beblob-popup-border: #c4c1bc; | ||
| --beblob-editor-bg: #f0eeed; | ||
| --beblob-editor-border: #d0cec9; | ||
| --beblob-editor-text: #2c2c2c; | ||
| --beblob-editor-placeholder: #999; | ||
| --beblob-disabled-bg: #d5d3d1; | ||
| --beblob-disabled-text: #aaa; | ||
| --beblob-disabled-border: #ccc; | ||
| --beblob-issues-bg: transparent; | ||
| --beblob-shadow: rgba(0, 0, 0, 0.06); | ||
| --beblob-cursor: #2c2c2c; | ||
| --beblob-font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | ||
| --beblob-radius: 6px; | ||
| --beblob-spinner-track: #d5d3d1; | ||
| --beblob-spinner-head: #999; | ||
| --beblob-toolbar-btn-bg: transparent; | ||
| --beblob-toolbar-btn-border: transparent; | ||
| --beblob-toolbar-btn-hover-bg: rgba(0, 0, 0, 0.08); | ||
| --beblob-toolbar-border: #d0cec9; | ||
| } | ||
| /* --- Dark Theme --- */ | ||
| .beblob-widget.theme-dark { | ||
| --beblob-bg: #1d1f21; | ||
| --beblob-text: #c9cacc; | ||
| --beblob-text-muted: #888; | ||
| --beblob-accent: #2a2d30; | ||
| --beblob-border: #383b40; | ||
| --beblob-link: #4cd69c; | ||
| --beblob-comment-bg: #25282c; | ||
| --beblob-comment-border: #383b40; | ||
| --beblob-button-bg: transparent; | ||
| --beblob-button-text: #c9cacc; | ||
| --beblob-button-border: #484d54; | ||
| --beblob-button-hover-bg: rgba(255, 255, 255, 0.08); | ||
| --beblob-tab-bg: transparent; | ||
| --beblob-tab-active-bg: #2bbc8a; | ||
| --beblob-tab-active-text: #1d1f21; | ||
| --beblob-reaction-bg: transparent; | ||
| --beblob-reaction-border: #484d54; | ||
| --beblob-reaction-hover-bg: rgba(255, 255, 255, 0.08); | ||
| --beblob-popup-bg: #2a2d30; | ||
| --beblob-popup-border: #484d54; | ||
| --beblob-editor-bg: #25282c; | ||
| --beblob-editor-border: #383b40; | ||
| --beblob-editor-text: #c9cacc; | ||
| --beblob-editor-placeholder: #666; | ||
| --beblob-disabled-bg: #2a2c2e; | ||
| --beblob-disabled-text: #555; | ||
| --beblob-disabled-border: #383b40; | ||
| --beblob-issues-bg: transparent; | ||
| --beblob-shadow: rgba(0, 0, 0, 0.3); | ||
| --beblob-cursor: #c9cacc; | ||
| --beblob-spinner-track: #333; | ||
| --beblob-spinner-head: #2bbc8a; | ||
| --beblob-toolbar-btn-bg: transparent; | ||
| --beblob-toolbar-btn-border: transparent; | ||
| --beblob-toolbar-btn-hover-bg: rgba(255, 255, 255, 0.1); | ||
| --beblob-toolbar-border: #383b40; | ||
| } | ||
| /* --- White Theme --- */ | ||
| .beblob-widget.theme-white { | ||
| --beblob-bg: #ffffff; | ||
| --beblob-text: #24292e; | ||
| --beblob-text-muted: #888; | ||
| --beblob-accent: #f6f8fa; | ||
| --beblob-border: #e1e4e8; | ||
| --beblob-link: #0366d6; | ||
| --beblob-comment-bg: #fafbfc; | ||
| --beblob-comment-border: #e1e4e8; | ||
| --beblob-button-bg: transparent; | ||
| --beblob-button-text: #24292e; | ||
| --beblob-button-border: #d1d5da; | ||
| --beblob-button-hover-bg: rgba(0, 0, 0, 0.04); | ||
| --beblob-tab-bg: transparent; | ||
| --beblob-tab-active-bg: #e1e4e8; | ||
| --beblob-tab-active-text: #24292e; | ||
| --beblob-reaction-bg: transparent; | ||
| --beblob-reaction-border: #d1d5da; | ||
| --beblob-reaction-hover-bg: rgba(0, 0, 0, 0.04); | ||
| --beblob-popup-bg: #f6f8fa; | ||
| --beblob-popup-border: #d1d5da; | ||
| --beblob-editor-bg: #fafbfc; | ||
| --beblob-editor-border: #e1e4e8; | ||
| --beblob-editor-text: #24292e; | ||
| --beblob-editor-placeholder: #999; | ||
| --beblob-disabled-bg: #f6f8fa; | ||
| --beblob-disabled-text: #aaa; | ||
| --beblob-disabled-border: #e1e4e8; | ||
| --beblob-issues-bg: transparent; | ||
| --beblob-shadow: rgba(0, 0, 0, 0.04); | ||
| --beblob-cursor: #24292e; | ||
| --beblob-spinner-track: #eee; | ||
| --beblob-spinner-head: #999; | ||
| --beblob-toolbar-btn-bg: transparent; | ||
| --beblob-toolbar-btn-border: transparent; | ||
| --beblob-toolbar-btn-hover-bg: rgba(0, 0, 0, 0.06); | ||
| --beblob-toolbar-border: #e1e4e8; | ||
| } | ||
| /* --- Light Theme (explicit, same as default) --- */ | ||
| .beblob-widget.theme-light { | ||
| /* Uses default variables */ | ||
| } | ||
| /* --- Classic Theme --- */ | ||
| .beblob-widget.theme-classic { | ||
| --beblob-bg: #fafafa; | ||
| --beblob-text: #111; | ||
| --beblob-text-muted: #888; | ||
| --beblob-accent: #f0f0f0; | ||
| --beblob-border: #ddd; | ||
| --beblob-link: #0366d6; | ||
| --beblob-comment-bg: #fff; | ||
| --beblob-comment-border: #ddd; | ||
| --beblob-button-bg: transparent; | ||
| --beblob-button-text: #111; | ||
| --beblob-button-border: #ccc; | ||
| --beblob-button-hover-bg: rgba(0, 0, 0, 0.05); | ||
| --beblob-tab-bg: transparent; | ||
| --beblob-tab-active-bg: #e0e0e0; | ||
| --beblob-tab-active-text: #111; | ||
| --beblob-reaction-bg: transparent; | ||
| --beblob-reaction-border: #ccc; | ||
| --beblob-reaction-hover-bg: rgba(0, 0, 0, 0.05); | ||
| --beblob-popup-bg: #eee; | ||
| --beblob-popup-border: #ccc; | ||
| --beblob-editor-bg: #fff; | ||
| --beblob-editor-border: #ddd; | ||
| --beblob-editor-text: #111; | ||
| --beblob-editor-placeholder: #999; | ||
| --beblob-disabled-bg: #eee; | ||
| --beblob-disabled-text: #aaa; | ||
| --beblob-disabled-border: #ccc; | ||
| --beblob-issues-bg: transparent; | ||
| --beblob-shadow: rgba(0, 0, 0, 0.05); | ||
| --beblob-cursor: #111; | ||
| --beblob-spinner-track: #eee; | ||
| --beblob-spinner-head: #999; | ||
| --beblob-toolbar-btn-bg: transparent; | ||
| --beblob-toolbar-btn-border: transparent; | ||
| --beblob-toolbar-btn-hover-bg: rgba(0, 0, 0, 0.06); | ||
| --beblob-toolbar-border: #ddd; | ||
| } | ||
| /* =========================== | ||
| BASE STYLES | ||
| =========================== */ | ||
| .beblob-widget { | ||
| position: relative; | ||
| min-height: 120px; | ||
| background-color: var(--beblob-bg); | ||
| color: var(--beblob-text); | ||
| font-family: var(--beblob-font-family); | ||
| border-radius: var(--beblob-radius); | ||
| padding: 20px; | ||
| transition: background-color 0.2s ease, color 0.2s ease; | ||
| } | ||
| .beblob-widget *, | ||
| .beblob-widget *::before, | ||
| .beblob-widget *::after { | ||
| box-sizing: border-box; | ||
| } | ||
| .beblob-widget a { | ||
| color: var(--beblob-link); | ||
| transition: color 0.15s ease; | ||
| } | ||
| .beblob-widget a:hover { | ||
| opacity: 0.8; | ||
| } | ||
| /* =========================== | ||
| ISSUE CONTAINER | ||
| =========================== */ | ||
| .issue { | ||
| margin-bottom: 0; | ||
| border: none; | ||
| padding: 0; | ||
| } | ||
| #issuesContainer { | ||
| padding: 0; | ||
| border-radius: 0; | ||
| margin-bottom: 4px; | ||
| border: none; | ||
| background-color: var(--beblob-issues-bg); | ||
| color: var(--beblob-text); | ||
| } | ||
| /* =========================== | ||
| COMMENTS | ||
| =========================== */ | ||
| .comment { | ||
| margin-bottom: 12px; | ||
| padding: 14px 16px; | ||
| border: 1px solid var(--beblob-comment-border); | ||
| border-radius: var(--beblob-radius); | ||
| background-color: var(--beblob-comment-bg); | ||
| color: var(--beblob-text); | ||
| transition: background-color 0.2s ease, border-color 0.2s ease; | ||
| } | ||
| .comment-header { | ||
| display: flex; | ||
| align-items: center; | ||
| margin-bottom: 10px; | ||
| padding-bottom: 8px; | ||
| border-bottom: 1px solid var(--beblob-border); | ||
| flex-wrap: wrap; | ||
| gap: 6px; | ||
| } | ||
| .comment-header img { | ||
| width: 28px; | ||
| height: 28px; | ||
| border-radius: 50%; | ||
| flex-shrink: 0; | ||
| } | ||
| .comment-header .author-info { | ||
| display: flex; | ||
| align-items: center; | ||
| flex-wrap: wrap; | ||
| gap: 4px; | ||
| font-size: 0.85rem; | ||
| } | ||
| .comment-header .author-info .author-name { | ||
| font-weight: 600; | ||
| color: var(--beblob-link); | ||
| text-decoration: none; | ||
| } | ||
| .comment-header .author-info .author-name:hover { | ||
| text-decoration: underline; | ||
| } | ||
| .comment-header .author-info .commented-on, | ||
| .comment-header .author-info .comment-timestamp { | ||
| color: var(--beblob-text-muted); | ||
| font-size: 0.8rem; | ||
| } | ||
| .comment-body { | ||
| line-height: 1.6; | ||
| font-size: 0.92rem; | ||
| } | ||
| .comment-body pre { | ||
| background-color: var(--beblob-accent); | ||
| padding: 12px; | ||
| border-radius: var(--beblob-radius); | ||
| overflow-x: auto; | ||
| border: 1px solid var(--beblob-border); | ||
| font-size: 0.85rem; | ||
| } | ||
| .comment-body code { | ||
| font-size: 0.85em; | ||
| } | ||
| .comment-body p:first-child { | ||
| margin-top: 0; | ||
| } | ||
| .comment-body p:last-child { | ||
| margin-bottom: 0; | ||
| } | ||
| .comments-label { | ||
| margin-bottom: 10px; | ||
| font-size: 0.8rem; | ||
| font-weight: 500; | ||
| color: var(--beblob-text-muted); | ||
| } | ||
| .comments-label a { | ||
| color: var(--beblob-text-muted); | ||
| text-decoration: none; | ||
| } | ||
| .comments-label a:hover { | ||
| text-decoration: underline; | ||
| } | ||
| /* No comments empty state */ | ||
| .beblob-empty-state { | ||
| text-align: center; | ||
| padding: 24px 16px; | ||
| color: var(--beblob-text-muted); | ||
| font-size: 0.9rem; | ||
| } | ||
| /* =========================== | ||
| COMMENT TEXTAREA & EDITOR | ||
| =========================== */ | ||
| .comment-textarea-container { | ||
| margin-top: 16px; | ||
| border-top: 1px solid var(--beblob-border); | ||
| padding-top: 16px; | ||
| position: relative; | ||
| } | ||
| .comment-textarea-container textarea { | ||
| width: 100%; | ||
| min-height: 80px; | ||
| padding: 10px; | ||
| border: 1px solid var(--beblob-editor-border); | ||
| border-radius: var(--beblob-radius); | ||
| margin-bottom: 8px; | ||
| resize: vertical; | ||
| background-color: var(--beblob-editor-bg); | ||
| color: var(--beblob-editor-text); | ||
| font-family: var(--beblob-font-family); | ||
| font-size: 0.9rem; | ||
| } | ||
| /* =========================== | ||
| CURRENT USER AVATAR | ||
| =========================== */ | ||
| .tab .current-user-avatar-container { | ||
| margin-left: auto; | ||
| display: flex; | ||
| align-items: center; | ||
| } | ||
| .tab .current-user-avatar-container img { | ||
| width: 24px; | ||
| height: 24px; | ||
| border-radius: 50%; | ||
| } | ||
| /* =========================== | ||
| BUTTONS | ||
| =========================== */ | ||
| .gl-button, | ||
| .gl-button.btn-block { | ||
| display: inline-flex; | ||
| justify-content: center; | ||
| align-items: center; | ||
| padding: 8px 16px; | ||
| background-color: var(--beblob-button-bg); | ||
| color: var(--beblob-button-text); | ||
| border: 1px solid var(--beblob-button-border); | ||
| font-size: 0.8rem; | ||
| font-family: var(--beblob-font-family); | ||
| border-radius: var(--beblob-radius); | ||
| cursor: pointer; | ||
| transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease; | ||
| font-weight: 500; | ||
| } | ||
| .gl-button:hover, | ||
| .gl-button.btn-block:hover { | ||
| background-color: var(--beblob-button-hover-bg); | ||
| } | ||
| .gl-button:focus-visible { | ||
| outline: 2px solid var(--beblob-link); | ||
| outline-offset: 2px; | ||
| } | ||
| /* Tab container */ | ||
| .tab { | ||
| display: flex; | ||
| align-items: center; | ||
| border-bottom: 1px solid var(--beblob-border); | ||
| margin-bottom: 0; | ||
| gap: 2px; | ||
| } | ||
| /* Tab buttons — NOT .gl-button style */ | ||
| .tab .tablinks { | ||
| background-color: var(--beblob-tab-bg); | ||
| color: var(--beblob-text-muted); | ||
| border: none; | ||
| border-top-left-radius: var(--beblob-radius); | ||
| border-top-right-radius: var(--beblob-radius); | ||
| padding: 8px 16px; | ||
| cursor: pointer; | ||
| font-family: var(--beblob-font-family); | ||
| font-size: 0.8rem; | ||
| font-weight: 500; | ||
| transition: background-color 0.15s ease, color 0.15s ease; | ||
| /* Reset .gl-button styles that tabs inherit */ | ||
| box-shadow: none; | ||
| } | ||
| .tab .tablinks:hover { | ||
| background-color: var(--beblob-button-hover-bg); | ||
| color: var(--beblob-text); | ||
| } | ||
| .tab .tablinks:focus-visible { | ||
| outline: 2px solid var(--beblob-link); | ||
| outline-offset: -2px; | ||
| } | ||
| .tab .tablinks.active { | ||
| background-color: var(--beblob-tab-active-bg); | ||
| color: var(--beblob-tab-active-text); | ||
| border-bottom: 1px solid transparent; | ||
| font-weight: 600; | ||
| } | ||
| /* Add Comment Button */ | ||
| #addCommentButton { | ||
| padding: 8px 16px; | ||
| border-radius: var(--beblob-radius); | ||
| cursor: pointer; | ||
| transition: background-color 0.15s ease, color 0.15s ease; | ||
| border: 1px solid var(--beblob-button-border); | ||
| background-color: var(--beblob-button-bg); | ||
| color: var(--beblob-button-text); | ||
| font-family: var(--beblob-font-family); | ||
| font-size: 0.8rem; | ||
| font-weight: 500; | ||
| } | ||
| #addCommentButton:hover { | ||
| background-color: var(--beblob-button-hover-bg); | ||
| } | ||
| #addCommentButton:disabled { | ||
| background-color: var(--beblob-disabled-bg); | ||
| color: var(--beblob-disabled-text); | ||
| border-color: var(--beblob-disabled-border); | ||
| cursor: not-allowed; | ||
| } | ||
| /* Auth Button */ | ||
| .auth-button { | ||
| gap: 6px; | ||
| } | ||
| .auth-button img.gitlab-logo { | ||
| width: 18px; | ||
| height: 18px; | ||
| } | ||
| /* =========================== | ||
| REACTIONS & POPUP | ||
| =========================== */ | ||
| .reactions-center { | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| flex-wrap: wrap; | ||
| gap: 6px; | ||
| margin-bottom: 8px; | ||
| } | ||
| .reaction-btn, | ||
| .reaction-toggle-btn, | ||
| .reaction-popup-btn { | ||
| display: inline-flex; | ||
| justify-content: center; | ||
| align-items: center; | ||
| min-height: 34px; | ||
| min-width: 34px; | ||
| padding: 4px 8px; | ||
| border: 1px solid var(--beblob-reaction-border); | ||
| background-color: var(--beblob-reaction-bg); | ||
| color: var(--beblob-text); | ||
| cursor: pointer; | ||
| font-size: 0.9em; | ||
| border-radius: var(--beblob-radius); | ||
| transition: background-color 0.15s ease, border-color 0.15s ease; | ||
| } | ||
| .reaction-btn:hover, | ||
| .reaction-toggle-btn:hover, | ||
| .reaction-popup-btn:hover { | ||
| background-color: var(--beblob-reaction-hover-bg); | ||
| } | ||
| .reaction-btn:focus-visible, | ||
| .reaction-toggle-btn:focus-visible, | ||
| .reaction-popup-btn:focus-visible { | ||
| outline: 2px solid var(--beblob-link); | ||
| outline-offset: 2px; | ||
| } | ||
| .reaction-toggle-btn svg { | ||
| width: 16px; | ||
| height: 16px; | ||
| fill: currentColor; | ||
| } | ||
| .reaction-container { | ||
| position: relative; | ||
| display: inline-block; | ||
| } | ||
| #reaction-popup { | ||
| position: absolute; | ||
| top: 100%; | ||
| left: 0; | ||
| padding: 4px; | ||
| z-index: 1000; | ||
| border-radius: var(--beblob-radius); | ||
| background-color: var(--beblob-popup-bg); | ||
| border: 1px solid var(--beblob-popup-border); | ||
| color: var(--beblob-text); | ||
| display: none; | ||
| white-space: nowrap; | ||
| box-shadow: 0 4px 12px var(--beblob-shadow); | ||
| } | ||
| #reaction-popup button { | ||
| display: inline-flex; | ||
| margin: 2px; | ||
| } | ||
| /* =========================== | ||
| COMMENT ACTIONS | ||
| =========================== */ | ||
| .comment-actions { | ||
| display: flex; | ||
| justify-content: space-between; | ||
| align-items: center; | ||
| margin-top: 10px; | ||
| gap: 8px; | ||
| } | ||
| /* =========================== | ||
| LOADING INDICATOR | ||
| =========================== */ | ||
| .loading-indicator { | ||
| position: absolute; | ||
| top: 50%; | ||
| left: 50%; | ||
| transform: translate(-50%, -50%); | ||
| display: flex; | ||
| flex-direction: column; | ||
| align-items: center; | ||
| justify-content: center; | ||
| width: 100%; | ||
| text-align: center; | ||
| font-size: 0.85rem; | ||
| color: var(--beblob-text-muted); | ||
| } | ||
| .spinner { | ||
| display: inline-block; | ||
| border: 2px solid var(--beblob-spinner-track); | ||
| border-top: 2px solid var(--beblob-spinner-head); | ||
| border-radius: 50%; | ||
| width: 24px; | ||
| height: 24px; | ||
| animation: beblob-spin 0.8s linear infinite; | ||
| margin: 0 auto 8px auto; | ||
| } | ||
| @keyframes beblob-spin { | ||
| 0% { transform: rotate(0deg); } | ||
| 100% { transform: rotate(360deg); } | ||
| } | ||
| /* =========================== | ||
| EDITOR TOOLBAR (EasyMDE/CodeMirror) | ||
| =========================== */ | ||
| .beblob-widget .editor-toolbar { | ||
| border: none; | ||
| border-bottom: 1px solid var(--beblob-toolbar-border); | ||
| background-color: transparent; | ||
| border-radius: 0; | ||
| padding: 4px 2px; | ||
| opacity: 0.7; | ||
| } | ||
| .beblob-widget .editor-toolbar:hover { | ||
| opacity: 1; | ||
| } | ||
| .beblob-widget.theme-dark .editor-toolbar { | ||
| opacity: 1; | ||
| } | ||
| .beblob-widget .editor-toolbar a, | ||
| .beblob-widget .editor-toolbar button { | ||
| display: inline-flex; | ||
| justify-content: center; | ||
| align-items: center; | ||
| text-decoration: none !important; | ||
| width: 28px; | ||
| height: 28px; | ||
| margin: 0 1px; | ||
| border-radius: 4px; | ||
| cursor: pointer; | ||
| background-color: var(--beblob-toolbar-btn-bg); | ||
| color: var(--beblob-text) !important; | ||
| border: 1px solid var(--beblob-toolbar-btn-border); | ||
| transition: background-color 0.15s ease; | ||
| } | ||
| .beblob-widget .editor-toolbar a:hover, | ||
| .beblob-widget .editor-toolbar button:hover { | ||
| background-color: var(--beblob-toolbar-btn-hover-bg) !important; | ||
| } | ||
| .beblob-widget .editor-toolbar button.active { | ||
| background-color: var(--beblob-toolbar-btn-hover-bg) !important; | ||
| border-color: var(--beblob-toolbar-btn-border) !important; | ||
| } | ||
| .beblob-widget .editor-toolbar i.separator { | ||
| border-left-color: var(--beblob-border); | ||
| } | ||
| /* CodeMirror (Markdown editor) */ | ||
| .beblob-widget #Preview, | ||
| .beblob-widget .CodeMirror { | ||
| background-color: var(--beblob-editor-bg); | ||
| border: 1px solid var(--beblob-editor-border); | ||
| color: var(--beblob-editor-text); | ||
| border-radius: 0 0 var(--beblob-radius) var(--beblob-radius); | ||
| transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease; | ||
| min-height: 80px; | ||
| font-size: 0.9rem; | ||
| } | ||
| .beblob-widget .CodeMirror pre, | ||
| .beblob-widget .CodeMirror-line { | ||
| color: inherit; | ||
| } | ||
| .beblob-widget .CodeMirror-placeholder { | ||
| color: var(--beblob-editor-placeholder); | ||
| } | ||
| .beblob-widget .CodeMirror-cursor { | ||
| border-left: 1px solid var(--beblob-cursor) !important; | ||
| } | ||
| /* EasyMDE container overrides */ | ||
| .beblob-widget .EasyMDEContainer { | ||
| border: 1px solid var(--beblob-editor-border); | ||
| border-radius: var(--beblob-radius); | ||
| overflow: hidden; | ||
| } | ||
| .beblob-widget .EasyMDEContainer .CodeMirror { | ||
| border: none; | ||
| border-radius: 0; | ||
| } | ||
| .beblob-widget .EasyMDEContainer .editor-toolbar { | ||
| border-bottom: 1px solid var(--beblob-editor-border); | ||
| } | ||
| /* Hide the EasyMDE status bar */ | ||
| .beblob-widget .editor-statusbar { | ||
| display: none; | ||
| } | ||
| /* Dark theme: code block and syntax highlighting overrides */ | ||
| .beblob-widget.theme-dark .comment-body pre { | ||
| background-color: var(--beblob-accent); | ||
| color: var(--beblob-text); | ||
| } | ||
| .beblob-widget.theme-dark .hljs { | ||
| background: var(--beblob-accent); | ||
| color: var(--beblob-text); | ||
| } | ||
| .beblob-widget.theme-dark .cm-s-default .cm-header { color: #7ec8e3; } | ||
| .beblob-widget.theme-dark .cm-s-default .cm-quote { color: #6bbd6b; } | ||
| .beblob-widget.theme-dark .cm-s-default .cm-comment { color: #888; } | ||
| .beblob-widget.theme-dark .cm-s-default .cm-string { color: #e09070; } | ||
| .beblob-widget.theme-dark .cm-s-default .cm-keyword { color: #c678dd; } | ||
| /* =========================== | ||
| MOBILE RESPONSIVE | ||
| =========================== */ | ||
| @media (max-width: 600px) { | ||
| .beblob-widget { | ||
| padding: 12px; | ||
| } | ||
| /* Stack comment header vertically on small screens */ | ||
| .comment-header { | ||
| flex-direction: column; | ||
| align-items: flex-start; | ||
| gap: 4px; | ||
| } | ||
| /* Touch-friendly buttons (min 44px tap target) */ | ||
| .gl-button, | ||
| .gl-button.btn-block, | ||
| .reaction-btn, | ||
| .reaction-toggle-btn, | ||
| .reaction-popup-btn { | ||
| min-height: 44px; | ||
| min-width: 44px; | ||
| padding: 10px 16px; | ||
| } | ||
| /* Stack comment actions vertically */ | ||
| .comment-actions { | ||
| flex-direction: column; | ||
| gap: 8px; | ||
| } | ||
| .comment-actions .gl-button, | ||
| .comment-actions .auth-button, | ||
| .comment-actions #addCommentButton { | ||
| width: 100%; | ||
| justify-content: center; | ||
| } | ||
| /* Tab buttons take equal space */ | ||
| .tab { | ||
| flex-wrap: wrap; | ||
| } | ||
| .tab .tablinks { | ||
| flex: 1; | ||
| text-align: center; | ||
| padding: 10px 12px; | ||
| } | ||
| /* Editor toolbar scrolls horizontally */ | ||
| .beblob-widget .editor-toolbar { | ||
| overflow-x: auto; | ||
| white-space: nowrap; | ||
| -webkit-overflow-scrolling: touch; | ||
| } | ||
| /* Reaction popup positioned better on mobile */ | ||
| #reaction-popup { | ||
| left: auto; | ||
| right: 0; | ||
| } | ||
| } |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 380 380"><defs><style>.cls-1{fill:#e24329;}.cls-2{fill:#fc6d26;}.cls-3{fill:#fca326;}</style></defs><g id="LOGO"><path class="cls-1" d="M282.83,170.73l-.27-.69-26.14-68.22a6.81,6.81,0,0,0-2.69-3.24,7,7,0,0,0-8,.43,7,7,0,0,0-2.32,3.52l-17.65,54H154.29l-17.65-54A6.86,6.86,0,0,0,134.32,99a7,7,0,0,0-8-.43,6.87,6.87,0,0,0-2.69,3.24L97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82,19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91,40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/><path class="cls-2" d="M282.83,170.73l-.27-.69a88.3,88.3,0,0,0-35.15,15.8L190,229.25c19.55,14.79,36.57,27.64,36.57,27.64l40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/><path class="cls-3" d="M153.43,256.89l19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91S209.55,244,190,229.25C170.45,244,153.43,256.89,153.43,256.89Z"/><path class="cls-2" d="M132.58,185.84A88.19,88.19,0,0,0,97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82s17-12.85,36.57-27.64Z"/></g></svg> |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 380 380"><defs><style>.cls-1{fill:#171321;}</style></defs><g id="LOGO"><path class="cls-1" d="M282.83,170.73l-.27-.69-26.14-68.22a6.81,6.81,0,0,0-2.69-3.24,7,7,0,0,0-8,.43,7,7,0,0,0-2.32,3.52l-17.65,54H154.29l-17.65-54A6.86,6.86,0,0,0,134.32,99a7,7,0,0,0-8-.43,6.87,6.87,0,0,0-2.69,3.24L97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82,19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91,40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/></g></svg> |
| /** | ||
| * Returns the GitLab API v4 base URL for the configured instance. | ||
| * @param {object} config - Widget configuration with gitlabUrl | ||
| * @returns {string} | ||
| */ | ||
| export function apiBaseUrl(config) { | ||
| return `${config.gitlabUrl}/api/v4`; | ||
| } | ||
| /** | ||
| * Returns the GitLab OAuth base URL for the configured instance. | ||
| * @param {object} config - Widget configuration with gitlabUrl | ||
| * @returns {string} | ||
| */ | ||
| export function oauthBaseUrl(config) { | ||
| return `${config.gitlabUrl}/oauth`; | ||
| } |
| import { describe, it, expect } from 'vitest'; | ||
| import { apiBaseUrl, oauthBaseUrl } from '../../src/js/api/gitlab-url.js'; | ||
| describe('gitlab-url helpers', () => { | ||
| describe('apiBaseUrl', () => { | ||
| it('returns default gitlab.com API URL', () => { | ||
| expect(apiBaseUrl({ gitlabUrl: 'https://gitlab.com' })).toBe( | ||
| 'https://gitlab.com/api/v4' | ||
| ); | ||
| }); | ||
| it('returns custom instance API URL', () => { | ||
| expect(apiBaseUrl({ gitlabUrl: 'https://gitlab.example.com' })).toBe( | ||
| 'https://gitlab.example.com/api/v4' | ||
| ); | ||
| }); | ||
| }); | ||
| describe('oauthBaseUrl', () => { | ||
| it('returns default gitlab.com OAuth URL', () => { | ||
| expect(oauthBaseUrl({ gitlabUrl: 'https://gitlab.com' })).toBe( | ||
| 'https://gitlab.com/oauth' | ||
| ); | ||
| }); | ||
| it('returns custom instance OAuth URL', () => { | ||
| expect(oauthBaseUrl({ gitlabUrl: 'https://gitlab.example.com' })).toBe( | ||
| 'https://gitlab.example.com/oauth' | ||
| ); | ||
| }); | ||
| }); | ||
| }); |
+0
-2
@@ -68,5 +68,3 @@ image: node:20 | ||
| script: | ||
| - test -n "$NPM_TOKEN" && echo "NPM_TOKEN is set" || echo "NPM_TOKEN is EMPTY" | ||
| - echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc | ||
| - npm whoami || echo "npm whoami failed" | ||
| - npm publish | ||
@@ -73,0 +71,0 @@ rules: |
+1
-0
@@ -106,2 +106,3 @@ <!DOCTYPE html> | ||
| data-theme="light" | ||
| <!-- data-gitlab-url="https://gitlab.example.com" --> | ||
| defer> | ||
@@ -108,0 +109,0 @@ </script> |
+30
-3
@@ -612,4 +612,8 @@ /* =========================== | ||
| } | ||
| .beblob-widget.theme-dark .editor-toolbar { | ||
| opacity: 1; | ||
| } | ||
| .beblob-widget .editor-toolbar a { | ||
| .beblob-widget .editor-toolbar a, | ||
| .beblob-widget .editor-toolbar button { | ||
| display: inline-flex; | ||
@@ -630,6 +634,12 @@ justify-content: center; | ||
| .beblob-widget .editor-toolbar a:hover { | ||
| background-color: var(--beblob-toolbar-btn-hover-bg); | ||
| .beblob-widget .editor-toolbar a:hover, | ||
| .beblob-widget .editor-toolbar button:hover { | ||
| background-color: var(--beblob-toolbar-btn-hover-bg) !important; | ||
| } | ||
| .beblob-widget .editor-toolbar button.active { | ||
| background-color: var(--beblob-toolbar-btn-hover-bg) !important; | ||
| border-color: var(--beblob-toolbar-btn-border) !important; | ||
| } | ||
| .beblob-widget .editor-toolbar i.separator { | ||
@@ -685,2 +695,19 @@ border-left-color: var(--beblob-border); | ||
| /* Dark theme: code block and syntax highlighting overrides */ | ||
| .beblob-widget.theme-dark .comment-body pre { | ||
| background-color: var(--beblob-accent); | ||
| color: var(--beblob-text); | ||
| } | ||
| .beblob-widget.theme-dark .hljs { | ||
| background: var(--beblob-accent); | ||
| color: var(--beblob-text); | ||
| } | ||
| .beblob-widget.theme-dark .cm-s-default .cm-header { color: #7ec8e3; } | ||
| .beblob-widget.theme-dark .cm-s-default .cm-quote { color: #6bbd6b; } | ||
| .beblob-widget.theme-dark .cm-s-default .cm-comment { color: #888; } | ||
| .beblob-widget.theme-dark .cm-s-default .cm-string { color: #e09070; } | ||
| .beblob-widget.theme-dark .cm-s-default .cm-keyword { color: #c678dd; } | ||
| /* =========================== | ||
@@ -687,0 +714,0 @@ MOBILE RESPONSIVE |
+2
-2
| { | ||
| "name": "beblob", | ||
| "version": "2.0.0", | ||
| "version": "2.1.0", | ||
| "description": "This is an open-source widget allowing bloggers to insert comment section to their blogs hosted on GitLab Pages. The comments use GitLab issues underneath.", | ||
@@ -14,3 +14,3 @@ "main": "dist/beblob.js", | ||
| "test:coverage": "vitest run --coverage", | ||
| "demo": "npx http-server demo -p 9090 -o" | ||
| "demo": "npm run build && cp dist/beblob.js demo/beblob.js && cp -r dist/css dist/images demo/ && npx http-server demo -p 9090 -o" | ||
| }, | ||
@@ -17,0 +17,0 @@ "repository": { |
+1
-0
@@ -58,2 +58,3 @@ # BeBlob | ||
| | `theme` | No | Built-in theme: `dark`, `white`, `light` (default), `classic` | | ||
| | `gitlabUrl` | No | GitLab instance URL (default: `https://gitlab.com`). Use for self-managed GitLab. | | ||
@@ -60,0 +61,0 @@ ## Themes |
+30
-3
@@ -612,4 +612,8 @@ /* =========================== | ||
| } | ||
| .beblob-widget.theme-dark .editor-toolbar { | ||
| opacity: 1; | ||
| } | ||
| .beblob-widget .editor-toolbar a { | ||
| .beblob-widget .editor-toolbar a, | ||
| .beblob-widget .editor-toolbar button { | ||
| display: inline-flex; | ||
@@ -630,6 +634,12 @@ justify-content: center; | ||
| .beblob-widget .editor-toolbar a:hover { | ||
| background-color: var(--beblob-toolbar-btn-hover-bg); | ||
| .beblob-widget .editor-toolbar a:hover, | ||
| .beblob-widget .editor-toolbar button:hover { | ||
| background-color: var(--beblob-toolbar-btn-hover-bg) !important; | ||
| } | ||
| .beblob-widget .editor-toolbar button.active { | ||
| background-color: var(--beblob-toolbar-btn-hover-bg) !important; | ||
| border-color: var(--beblob-toolbar-btn-border) !important; | ||
| } | ||
| .beblob-widget .editor-toolbar i.separator { | ||
@@ -685,2 +695,19 @@ border-left-color: var(--beblob-border); | ||
| /* Dark theme: code block and syntax highlighting overrides */ | ||
| .beblob-widget.theme-dark .comment-body pre { | ||
| background-color: var(--beblob-accent); | ||
| color: var(--beblob-text); | ||
| } | ||
| .beblob-widget.theme-dark .hljs { | ||
| background: var(--beblob-accent); | ||
| color: var(--beblob-text); | ||
| } | ||
| .beblob-widget.theme-dark .cm-s-default .cm-header { color: #7ec8e3; } | ||
| .beblob-widget.theme-dark .cm-s-default .cm-quote { color: #6bbd6b; } | ||
| .beblob-widget.theme-dark .cm-s-default .cm-comment { color: #888; } | ||
| .beblob-widget.theme-dark .cm-s-default .cm-string { color: #e09070; } | ||
| .beblob-widget.theme-dark .cm-s-default .cm-keyword { color: #c678dd; } | ||
| /* =========================== | ||
@@ -687,0 +714,0 @@ MOBILE RESPONSIVE |
| import { state } from '../state.js'; | ||
| import { apiBaseUrl } from './gitlab-url.js'; | ||
@@ -7,7 +8,8 @@ /** | ||
| * @param {string} commentBody - The comment text | ||
| * @param {object} config - Widget configuration | ||
| * @returns {object|null} The created note or null on failure | ||
| */ | ||
| export async function addCommentToIssue(accessToken, commentBody) { | ||
| export async function addCommentToIssue(accessToken, commentBody, config) { | ||
| try { | ||
| const apiUrl = `https://gitlab.com/api/v4/projects/${state.projectId}/issues/${state.currentIssueId}/notes`; | ||
| const apiUrl = `${apiBaseUrl(config)}/projects/${state.projectId}/issues/${state.currentIssueId}/notes`; | ||
| const response = await fetch(apiUrl, { | ||
@@ -14,0 +16,0 @@ method: 'POST', |
+17
-7
| import { state } from '../state.js'; | ||
| import { IssueFetchStrategy } from '../constants.js'; | ||
| import { apiBaseUrl } from './gitlab-url.js'; | ||
@@ -9,7 +10,8 @@ /** | ||
| * @param {string} [description] | ||
| * @param {object} config - Widget configuration | ||
| * @returns {object|null} The created issue or null on failure | ||
| */ | ||
| export async function createIssue(accessToken, title, description) { | ||
| export async function createIssue(accessToken, title, description, config) { | ||
| try { | ||
| const apiUrl = `https://gitlab.com/api/v4/projects/${state.projectId}/issues`; | ||
| const apiUrl = `${apiBaseUrl(config)}/projects/${state.projectId}/issues`; | ||
| const response = await fetch(apiUrl, { | ||
@@ -38,7 +40,14 @@ method: 'POST', | ||
| * @param {string} fetchParam - URL, page title, or issue ID | ||
| * @param {object} config - Widget configuration | ||
| * @returns {object|null} The found issue or null | ||
| */ | ||
| export async function fetchGitLabIssue(accessToken, fetchType, fetchParam) { | ||
| export async function fetchGitLabIssue( | ||
| accessToken, | ||
| fetchType, | ||
| fetchParam, | ||
| config | ||
| ) { | ||
| try { | ||
| let apiUrl = `https://gitlab.com/api/v4/projects/${state.projectId}/issues`; | ||
| const base = apiBaseUrl(config); | ||
| let apiUrl = `${base}/projects/${state.projectId}/issues`; | ||
| if (fetchType === IssueFetchStrategy.URL) { | ||
@@ -49,3 +58,3 @@ apiUrl += `?search=${encodeURIComponent(fetchParam)}`; | ||
| } else if (fetchType === IssueFetchStrategy.ISSUE_ID) { | ||
| apiUrl = `https://gitlab.com/api/v4/projects/${state.projectId}/issues/${fetchParam}`; | ||
| apiUrl = `${base}/projects/${state.projectId}/issues/${fetchParam}`; | ||
| } | ||
@@ -82,8 +91,9 @@ | ||
| * @param {number} issueIid | ||
| * @param {object} config - Widget configuration | ||
| * @returns {Array} Array of discussion objects | ||
| */ | ||
| export async function fetchIssueDiscussions(accessToken, issueIid) { | ||
| export async function fetchIssueDiscussions(accessToken, issueIid, config) { | ||
| try { | ||
| const response = await fetch( | ||
| `https://gitlab.com/api/v4/projects/${state.projectId}/issues/${issueIid}/discussions`, | ||
| `${apiBaseUrl(config)}/projects/${state.projectId}/issues/${issueIid}/discussions`, | ||
| { headers: { Authorization: `Bearer ${accessToken}` } } | ||
@@ -90,0 +100,0 @@ ); |
| import { state } from '../state.js'; | ||
| import { apiBaseUrl } from './gitlab-url.js'; | ||
@@ -8,7 +9,8 @@ /** | ||
| * @param {string} projectName | ||
| * @param {object} config - Widget configuration | ||
| * @returns {number|null} The project ID or null on failure | ||
| */ | ||
| export async function fetchProjectId(accessToken, projectName) { | ||
| export async function fetchProjectId(accessToken, projectName, config) { | ||
| try { | ||
| const url = `https://gitlab.com/api/v4/projects?search=${encodeURIComponent(projectName)}`; | ||
| const url = `${apiBaseUrl(config)}/projects?search=${encodeURIComponent(projectName)}`; | ||
| const response = await fetch(url, { | ||
@@ -15,0 +17,0 @@ headers: { Authorization: `Bearer ${accessToken}` }, |
+19
-14
| import { state } from '../state.js'; | ||
| import { apiBaseUrl } from './gitlab-url.js'; | ||
@@ -7,7 +8,8 @@ /** | ||
| * @param {number} issueIid | ||
| * @param {object} config - Widget configuration | ||
| * @returns {Array} Array of reaction objects | ||
| */ | ||
| export async function fetchReactions(accessToken, issueIid) { | ||
| export async function fetchReactions(accessToken, issueIid, config) { | ||
| try { | ||
| const apiUrl = `https://gitlab.com/api/v4/projects/${state.projectId}/issues/${issueIid}/award_emoji`; | ||
| const apiUrl = `${apiBaseUrl(config)}/projects/${state.projectId}/issues/${issueIid}/award_emoji`; | ||
| const response = await fetch(apiUrl, { | ||
@@ -31,7 +33,8 @@ headers: { Authorization: `Bearer ${accessToken}` }, | ||
| * @param {string} reactionName | ||
| * @param {object} config - Widget configuration | ||
| * @returns {object} The created reaction | ||
| * @throws {Error} on failure (including 'already been taken') | ||
| */ | ||
| export async function addReaction(accessToken, issueIid, reactionName) { | ||
| const apiUrl = `https://gitlab.com/api/v4/projects/${state.projectId}/issues/${issueIid}/award_emoji`; | ||
| export async function addReaction(accessToken, issueIid, reactionName, config) { | ||
| const apiUrl = `${apiBaseUrl(config)}/projects/${state.projectId}/issues/${issueIid}/award_emoji`; | ||
| const response = await fetch(apiUrl, { | ||
@@ -56,7 +59,8 @@ method: 'POST', | ||
| * @param {number} awardId | ||
| * @param {object} config - Widget configuration | ||
| * @returns {boolean} true on success | ||
| */ | ||
| export async function removeReaction(accessToken, issueIid, awardId) { | ||
| export async function removeReaction(accessToken, issueIid, awardId, config) { | ||
| try { | ||
| const apiUrl = `https://gitlab.com/api/v4/projects/${state.projectId}/issues/${issueIid}/award_emoji/${awardId}`; | ||
| const apiUrl = `${apiBaseUrl(config)}/projects/${state.projectId}/issues/${issueIid}/award_emoji/${awardId}`; | ||
| const response = await fetch(apiUrl, { | ||
@@ -82,10 +86,11 @@ method: 'DELETE', | ||
| * @param {number} issueIid | ||
| * @param {object} config - Widget configuration | ||
| * @returns {Array} Updated reactions array | ||
| */ | ||
| export async function toggleReaction(name, accessToken, issueIid) { | ||
| export async function toggleReaction(name, accessToken, issueIid, config) { | ||
| if (!state.currentUser) { | ||
| return await fetchReactions(accessToken, issueIid); | ||
| return await fetchReactions(accessToken, issueIid, config); | ||
| } | ||
| const reactions = await fetchReactions(accessToken, issueIid); | ||
| const reactions = await fetchReactions(accessToken, issueIid, config); | ||
| const existing = reactions.find( | ||
@@ -96,9 +101,9 @@ (r) => r.name === name && r.user && r.user.id === state.currentUser.id | ||
| if (existing) { | ||
| await removeReaction(accessToken, issueIid, existing.id); | ||
| await removeReaction(accessToken, issueIid, existing.id, config); | ||
| } else { | ||
| try { | ||
| await addReaction(accessToken, issueIid, name); | ||
| await addReaction(accessToken, issueIid, name, config); | ||
| } catch (err) { | ||
| if (err.message.indexOf('already been taken') !== -1) { | ||
| const refreshed = await fetchReactions(accessToken, issueIid); | ||
| const refreshed = await fetchReactions(accessToken, issueIid, config); | ||
| const dup = refreshed.find( | ||
@@ -108,3 +113,3 @@ (r) => r.name === name && r.user && r.user.id === state.currentUser.id | ||
| if (dup) { | ||
| await removeReaction(accessToken, issueIid, dup.id); | ||
| await removeReaction(accessToken, issueIid, dup.id, config); | ||
| } | ||
@@ -117,3 +122,3 @@ } else { | ||
| return await fetchReactions(accessToken, issueIid); | ||
| return await fetchReactions(accessToken, issueIid, config); | ||
| } |
| import { state } from '../state.js'; | ||
| import { apiBaseUrl } from './gitlab-url.js'; | ||
@@ -7,7 +8,8 @@ /** | ||
| * @param {string} accessToken | ||
| * @param {object} config - Widget configuration | ||
| * @returns {object|null} The user object or null on failure | ||
| */ | ||
| export async function fetchCurrentUser(accessToken) { | ||
| export async function fetchCurrentUser(accessToken, config) { | ||
| try { | ||
| const response = await fetch('https://gitlab.com/api/v4/user', { | ||
| const response = await fetch(`${apiBaseUrl(config)}/user`, { | ||
| headers: { Authorization: `Bearer ${accessToken}` }, | ||
@@ -14,0 +16,0 @@ }); |
+3
-2
| import { LOCAL_STORAGE_KEY } from './constants.js'; | ||
| import { getAssetUrl } from './css-loader.js'; | ||
| import { oauthBaseUrl } from './api/gitlab-url.js'; | ||
@@ -11,3 +12,3 @@ /** | ||
| const state = encodeURIComponent(originalUrl); | ||
| const oauthUrl = `https://gitlab.com/oauth/authorize?client_id=${config.clientId}&redirect_uri=${encodeURIComponent(config.redirectUri)}&response_type=code&state=${state}`; | ||
| const oauthUrl = `${oauthBaseUrl(config)}/authorize?client_id=${config.clientId}&redirect_uri=${encodeURIComponent(config.redirectUri)}&response_type=code&state=${state}`; | ||
| window.location.href = oauthUrl; | ||
@@ -41,3 +42,3 @@ } | ||
| export async function requestAccessToken(code, config) { | ||
| const tokenUrl = 'https://gitlab.com/oauth/token'; | ||
| const tokenUrl = `${oauthBaseUrl(config)}/token`; | ||
| const params = { | ||
@@ -44,0 +45,0 @@ client_id: config.clientId, |
+4
-0
@@ -22,2 +22,6 @@ /** | ||
| theme: script.dataset.theme || 'light', | ||
| gitlabUrl: (script.dataset.gitlabUrl || 'https://gitlab.com').replace( | ||
| /\/+$/, | ||
| '' | ||
| ), | ||
| }; | ||
@@ -24,0 +28,0 @@ |
+16
-8
@@ -84,3 +84,3 @@ import EasyMDE from 'easymde'; | ||
| if (!storedToken || !state.projectId || !state.currentIssueId) return; | ||
| await addCommentToIssue(storedToken, comment); | ||
| await addCommentToIssue(storedToken, comment, config); | ||
| editor.value(''); | ||
@@ -132,3 +132,7 @@ await fetchAndDisplayIssue(storedToken, config, markedInstance); | ||
| async function loadProjectAndIssue(accessToken, config, markedInstance) { | ||
| const projectId = await fetchProjectId(accessToken, config.projectName); | ||
| const projectId = await fetchProjectId( | ||
| accessToken, | ||
| config.projectName, | ||
| config | ||
| ); | ||
| if (!projectId) { | ||
@@ -141,3 +145,3 @@ localStorage.removeItem(LOCAL_STORAGE_KEY); | ||
| await fetchAndDisplayIssue(accessToken, config, markedInstance); | ||
| const user = await fetchCurrentUser(accessToken); | ||
| const user = await fetchCurrentUser(accessToken, config); | ||
| if (user) { | ||
@@ -161,3 +165,4 @@ displayCurrentUserAvatar(user); | ||
| IssueFetchStrategy.URL, | ||
| window.location.href | ||
| window.location.href, | ||
| config | ||
| ); | ||
@@ -168,3 +173,4 @@ } else if (strategy === IssueFetchStrategy.PAGE_TITLE) { | ||
| IssueFetchStrategy.PAGE_TITLE, | ||
| document.title | ||
| document.title, | ||
| config | ||
| ); | ||
@@ -175,3 +181,4 @@ } else if (strategy === IssueFetchStrategy.ISSUE_ID) { | ||
| IssueFetchStrategy.ISSUE_ID, | ||
| config.issueId | ||
| config.issueId, | ||
| config | ||
| ); | ||
@@ -189,3 +196,4 @@ } | ||
| document.title, | ||
| 'Automatically created by BeBlob.' | ||
| 'Automatically created by BeBlob.', | ||
| config | ||
| ); | ||
@@ -198,3 +206,3 @@ if (issue) { | ||
| if (issue) { | ||
| await displayIssue(issue, accessToken, markedInstance); | ||
| await displayIssue(issue, accessToken, markedInstance, config); | ||
| } | ||
@@ -201,0 +209,0 @@ } catch (error) { |
@@ -11,4 +11,5 @@ import { renderMarkdown } from '../markdown.js'; | ||
| * @param {Marked} markedInstance | ||
| * @param {object} config - Widget configuration | ||
| */ | ||
| export async function displayIssue(issue, accessToken, markedInstance) { | ||
| export async function displayIssue(issue, accessToken, markedInstance, config) { | ||
| const issuesContainer = document.getElementById('issuesContainer'); | ||
@@ -36,3 +37,7 @@ if (!issuesContainer) return; | ||
| const discussions = await fetchIssueDiscussions(accessToken, issue.iid); | ||
| const discussions = await fetchIssueDiscussions( | ||
| accessToken, | ||
| issue.iid, | ||
| config | ||
| ); | ||
| if (Array.isArray(discussions)) { | ||
@@ -62,4 +67,4 @@ discussions.forEach((discussion) => { | ||
| const reactions = await fetchReactions(accessToken, issue.iid); | ||
| renderReactions(reactions, accessToken, issue.iid); | ||
| const reactions = await fetchReactions(accessToken, issue.iid, config); | ||
| renderReactions(reactions, accessToken, issue.iid, config); | ||
| } | ||
@@ -66,0 +71,0 @@ |
@@ -10,4 +10,5 @@ import { predefinedReactions, emojiMap } from '../constants.js'; | ||
| * @param {number} issueIid | ||
| * @param {object} config - Widget configuration | ||
| */ | ||
| export function renderReactions(reactions, accessToken, issueIid) { | ||
| export function renderReactions(reactions, accessToken, issueIid, config) { | ||
| const container = document.getElementById('reactionsContainer'); | ||
@@ -40,3 +41,3 @@ if (!container) return; | ||
| issueIid, | ||
| reactions | ||
| config | ||
| ); | ||
@@ -55,3 +56,3 @@ centerDiv.appendChild(btn); | ||
| issueIid, | ||
| reactions | ||
| config | ||
| ); | ||
@@ -92,4 +93,9 @@ centerDiv.appendChild(btn); | ||
| btn.addEventListener('click', async () => { | ||
| const updated = await toggleReaction(name, accessToken, issueIid); | ||
| renderReactions(updated, accessToken, issueIid); | ||
| const updated = await toggleReaction( | ||
| name, | ||
| accessToken, | ||
| issueIid, | ||
| config | ||
| ); | ||
| renderReactions(updated, accessToken, issueIid, config); | ||
| popup.style.display = 'none'; | ||
@@ -106,3 +112,3 @@ }); | ||
| function createReactionButton(name, count, accessToken, issueIid) { | ||
| function createReactionButton(name, count, accessToken, issueIid, config) { | ||
| const btn = document.createElement('button'); | ||
@@ -112,6 +118,6 @@ btn.className = 'reaction-btn'; | ||
| btn.addEventListener('click', async () => { | ||
| const updated = await toggleReaction(name, accessToken, issueIid); | ||
| renderReactions(updated, accessToken, issueIid); | ||
| const updated = await toggleReaction(name, accessToken, issueIid, config); | ||
| renderReactions(updated, accessToken, issueIid, config); | ||
| }); | ||
| return btn; | ||
| } |
| import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; | ||
| import { state } from '../../src/js/state.js'; | ||
| const defaultConfig = { gitlabUrl: 'https://gitlab.com' }; | ||
| describe('api/issues', () => { | ||
@@ -28,3 +30,8 @@ beforeEach(() => { | ||
| ); | ||
| await fetchGitLabIssue('token', 'url', 'https://example.com/page'); | ||
| await fetchGitLabIssue( | ||
| 'token', | ||
| 'url', | ||
| 'https://example.com/page', | ||
| defaultConfig | ||
| ); | ||
@@ -50,3 +57,8 @@ expect(mockFetch).toHaveBeenCalledWith( | ||
| ); | ||
| await fetchGitLabIssue('token', 'pageTitle', 'My Blog / Post Title'); | ||
| await fetchGitLabIssue( | ||
| 'token', | ||
| 'pageTitle', | ||
| 'My Blog / Post Title', | ||
| defaultConfig | ||
| ); | ||
@@ -72,3 +84,3 @@ // Note: split('/').pop() keeps the leading space: ' Post Title' | ||
| ); | ||
| await fetchGitLabIssue('token', 'issueId', '42'); | ||
| await fetchGitLabIssue('token', 'issueId', '42', defaultConfig); | ||
@@ -93,3 +105,8 @@ expect(mockFetch).toHaveBeenCalledWith( | ||
| ); | ||
| const issue = await fetchGitLabIssue('token', 'url', 'https://x.com'); | ||
| const issue = await fetchGitLabIssue( | ||
| 'token', | ||
| 'url', | ||
| 'https://x.com', | ||
| defaultConfig | ||
| ); | ||
@@ -112,6 +129,31 @@ expect(issue.iid).toBe(99); | ||
| ); | ||
| const result = await fetchGitLabIssue('token', 'url', 'https://x.com'); | ||
| const result = await fetchGitLabIssue( | ||
| 'token', | ||
| 'url', | ||
| 'https://x.com', | ||
| defaultConfig | ||
| ); | ||
| expect(result).toBeNull(); | ||
| }); | ||
| it('uses custom gitlabUrl in API calls', async () => { | ||
| const mockFetch = vi.fn().mockResolvedValue({ | ||
| ok: true, | ||
| json: () => Promise.resolve([{ iid: 1 }]), | ||
| }); | ||
| vi.stubGlobal('fetch', mockFetch); | ||
| const { fetchGitLabIssue } = await import( | ||
| '../../src/js/api/issues.js' | ||
| ); | ||
| await fetchGitLabIssue('token', 'url', 'https://x.com', { | ||
| gitlabUrl: 'https://gitlab.example.com', | ||
| }); | ||
| expect(mockFetch).toHaveBeenCalledWith( | ||
| expect.stringContaining('https://gitlab.example.com/api/v4/'), | ||
| expect.any(Object) | ||
| ); | ||
| }); | ||
| }); | ||
@@ -131,3 +173,3 @@ | ||
| const { createIssue } = await import('../../src/js/api/issues.js'); | ||
| const result = await createIssue('token', 'New', 'Desc'); | ||
| const result = await createIssue('token', 'New', 'Desc', defaultConfig); | ||
@@ -144,3 +186,3 @@ expect(result).toEqual(mockIssue); | ||
| const { createIssue } = await import('../../src/js/api/issues.js'); | ||
| const result = await createIssue('token', 'New', 'Desc'); | ||
| const result = await createIssue('token', 'New', 'Desc', defaultConfig); | ||
@@ -165,3 +207,3 @@ expect(result).toBeNull(); | ||
| ); | ||
| const result = await fetchIssueDiscussions('token', 1); | ||
| const result = await fetchIssueDiscussions('token', 1, defaultConfig); | ||
@@ -180,3 +222,3 @@ expect(result).toEqual(discussions); | ||
| ); | ||
| const result = await fetchIssueDiscussions('token', 1); | ||
| const result = await fetchIssueDiscussions('token', 1, defaultConfig); | ||
@@ -183,0 +225,0 @@ expect(result).toEqual([]); |
+33
-7
@@ -41,9 +41,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; | ||
| it('stores token on successful exchange', async () => { | ||
| vi.stubGlobal( | ||
| 'fetch', | ||
| vi.fn().mockResolvedValue({ | ||
| ok: true, | ||
| json: () => Promise.resolve({ access_token: 'abc123' }), | ||
| }) | ||
| ); | ||
| const mockFetch = vi.fn().mockResolvedValue({ | ||
| ok: true, | ||
| json: () => Promise.resolve({ access_token: 'abc123' }), | ||
| }); | ||
| vi.stubGlobal('fetch', mockFetch); | ||
@@ -54,2 +52,3 @@ const { requestAccessToken } = await import('../src/js/auth.js'); | ||
| redirectUri: 'https://test.com/', | ||
| gitlabUrl: 'https://gitlab.com', | ||
| }); | ||
@@ -59,2 +58,6 @@ | ||
| expect(localStorage.getItem(LOCAL_STORAGE_KEY)).toBe('abc123'); | ||
| expect(mockFetch).toHaveBeenCalledWith( | ||
| 'https://gitlab.com/oauth/token', | ||
| expect.any(Object) | ||
| ); | ||
@@ -64,2 +67,24 @@ vi.unstubAllGlobals(); | ||
| it('uses custom gitlabUrl for token exchange', async () => { | ||
| const mockFetch = vi.fn().mockResolvedValue({ | ||
| ok: true, | ||
| json: () => Promise.resolve({ access_token: 'custom-token' }), | ||
| }); | ||
| vi.stubGlobal('fetch', mockFetch); | ||
| const { requestAccessToken } = await import('../src/js/auth.js'); | ||
| await requestAccessToken('auth-code', { | ||
| clientId: 'cid', | ||
| redirectUri: 'https://test.com/', | ||
| gitlabUrl: 'https://gitlab.example.com', | ||
| }); | ||
| expect(mockFetch).toHaveBeenCalledWith( | ||
| 'https://gitlab.example.com/oauth/token', | ||
| expect.any(Object) | ||
| ); | ||
| vi.unstubAllGlobals(); | ||
| }); | ||
| it('returns null on failed exchange', async () => { | ||
@@ -75,2 +100,3 @@ vi.stubGlobal( | ||
| redirectUri: 'https://test.com/', | ||
| gitlabUrl: 'https://gitlab.com', | ||
| }); | ||
@@ -77,0 +103,0 @@ |
+29
-0
@@ -86,4 +86,33 @@ import { describe, it, expect, beforeEach } from 'vitest'; | ||
| expect(config.theme).toBe('dark'); | ||
| expect(config.gitlabUrl).toBe('https://gitlab.com'); | ||
| }); | ||
| it('reads custom gitlabUrl from data attribute', () => { | ||
| const script = document.createElement('script'); | ||
| script.id = 'beblob-script'; | ||
| script.dataset.clientId = 'my-client'; | ||
| script.dataset.redirectUri = 'https://test.com/'; | ||
| script.dataset.projectName = 'my-project'; | ||
| script.dataset.issueMappingStrategy = 'url'; | ||
| script.dataset.gitlabUrl = 'https://gitlab.example.com'; | ||
| document.body.appendChild(script); | ||
| const config = readConfig(); | ||
| expect(config.gitlabUrl).toBe('https://gitlab.example.com'); | ||
| }); | ||
| it('strips trailing slashes from gitlabUrl', () => { | ||
| const script = document.createElement('script'); | ||
| script.id = 'beblob-script'; | ||
| script.dataset.clientId = 'my-client'; | ||
| script.dataset.redirectUri = 'https://test.com/'; | ||
| script.dataset.projectName = 'my-project'; | ||
| script.dataset.issueMappingStrategy = 'url'; | ||
| script.dataset.gitlabUrl = 'https://gitlab.example.com///'; | ||
| document.body.appendChild(script); | ||
| const config = readConfig(); | ||
| expect(config.gitlabUrl).toBe('https://gitlab.example.com'); | ||
| }); | ||
| it('defaults theme to light', () => { | ||
@@ -90,0 +119,0 @@ const script = document.createElement('script'); |
Sorry, the diff of this file is too big to display
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
2867556
93.51%53
12.77%3849
30.65%206
0.49%10
-9.09%