Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

117 changes: 117 additions & 0 deletions src/components/access-control-modal/AccessControlAddAccess.styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
:host {
.access-grants-form {
display: flex;
flex-direction: column;
padding: 10px;
align-items: flex-start;
gap: 10px;
align-self: stretch;
font-size: var(--solid-ui-font-size-sm, .875rem);

--access-role-select-width: 129px;

p {
color: var(--solid-ui-color-gray-600, #4A5565);
line-height: 1.5;
}

.access-grants-form-main {
display: flex;
align-items: flex-start;
gap: 10px;
align-self: stretch;

.access-grants-input {
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-width: 0;
padding: 10px 15px;
gap: 12px;
align-items: flex-start;
border-radius: 5px;
border: 1px solid var(--solid-ui-color-slate-200, #E2E8F0);
background: var(--solid-ui-color-white, #FFF);

solid-ui-combobox {
width: 100%;
--solid-ui-input-border-color: var(--solid-ui-color-slate-200, #E2E8F0);
}

.access-principal-error {
margin: 0;
color: var(--solid-ui-color-red-700, #B42318);
font-size: var(--solid-ui-font-size-xs, .75rem);
line-height: 1.4;
}
}
}

.access-grants-pending {
display: flex;
flex-wrap: wrap;
gap: 8px;
min-height: 42px;
align-items: center;
}

.access-grants-pending-item {
display: flex;
padding: 5px 10px;
align-items: center;
gap: 6px;
max-width: 100%;
border-radius: 5px;
border: 1px solid var(--solid-ui-color-gray-300, #D1D5DC);
background: var(--solid-ui-color-gray-50, #F9FAFB);
color: var(--solid-ui-color-gray-700, #374151);
}

.access-grants-pending-item-label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--solid-ui-color-gray-500, #6A7282);
font-size: var(--solid-ui-font-size-xs, .75rem);
line-height: 1;
}

.access-grants-pending-item-remove {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
border: none;
border-radius: 999px;
background: transparent;
color: var(--solid-ui-color-gray-300, #D1D5DC);
cursor: pointer;

icon-lucide-circle-x {
width: 14px;
height: 14px;
}
}
}

.access-grants-directory-entry {
display: inline-flex;
align-items: center;
gap: 8px;
line-height: 1;
}

.access-role-select {
display: block;
max-width: 100%;
margin-top: 25px;
flex: 0 0 auto;
width: var(--access-role-select-width);
--solid-ui-input-border-color: var(--solid-ui-color-slate-200, #E2E8F0);
}

.access-role-select--top {
margin-top: 0;
}
}
174 changes: 174 additions & 0 deletions src/components/access-control-modal/AccessControlAddAccess.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { customElement, WebComponent } from '@/lib/components'
import { html, nothing } from 'lit'
import { property } from 'lit/decorators.js'
import { ACCESS_ROLES, type AccessRole, type DirectoryEntry } from 'solid-logic'
import type { AsyncComboboxOptionsProvider, ComboboxChangeEvent } from '@/components/combobox'

import '~icons/lucide/book-user'
import '~icons/lucide/user-round'
import '~icons/lucide/users'
import '~icons/lucide/circle-x'
import '@/components/button'
import '@/components/combobox'
import '@/components/combobox-option'

import styles from './AccessControlAddAccess.styles.css'
import type { PendingAccessGrant, PendingGrantRemovedDetail, RoleChangedDetail, TargetInputDetail, TargetSelectedDetail } from './types'

export function directoryEntryOptionTemplate (entry: DirectoryEntry) {
return html`
<span class="access-grants-directory-entry">
${renderDirectoryEntryIcon(entry)}
<span>${entry.label}</span>
</span>
`
}

function renderDirectoryEntryIcon (entry: DirectoryEntry) {
if (entry.sources.includes('contacts') || entry.sources.includes('groups')) {
return html`<icon-lucide-book-user style="width: 13px; height: 13px; flex: 0 0 13px;"></icon-lucide-book-user>`
}

if (entry.sources.includes('friends')) {
return html`<icon-lucide-users style="width: 13px; height: 13px; flex: 0 0 13px;"></icon-lucide-users>`
}

if (entry.sources.includes('catalog')) {
return html`<icon-lucide-user-round style="width: 13px; height: 13px; flex: 0 0 13px;"></icon-lucide-user-round>`
}

return nothing
}

@customElement('solid-ui-access-control-add-access')
export default class AccessControlAddAccess extends WebComponent {
static styles = styles

@property({ attribute: false })
accessor principalInputValue = ''

@property({ attribute: false })
accessor addAccessRoleValue: AccessRole = 'Viewer'

@property({ attribute: false })
accessor pendingAccessGrants: PendingAccessGrant[] = []

@property({ attribute: false })
accessor inputError = ''

@property({ attribute: false })
accessor asyncOptionsProvider: AsyncComboboxOptionsProvider | null = null

private onPrincipalInput (event: Event) {
const value = this.getEventValue(event)
this.dispatchEvent(new CustomEvent<TargetInputDetail>('target-input', {
detail: { value },
bubbles: true,
composed: true
}))
}

private onPrincipalSelect (event: ComboboxChangeEvent) {
const option = event.detail.option
if (typeof option.value !== 'string') {
return
}

this.dispatchEvent(new CustomEvent<TargetSelectedDetail>('target-selected', {
detail: {
value: option.value,
label: option.label
},
bubbles: true,
composed: true
}))
}

private onRoleChange (event: ComboboxChangeEvent) {
const role = event.detail.option.value
if (typeof role !== 'string' || !ACCESS_ROLES.includes(role as AccessRole)) {
return
}

this.dispatchEvent(new CustomEvent<RoleChangedDetail>('role-changed', {
detail: { role: role as AccessRole },
bubbles: true,
composed: true
}))
}

private onPendingGrantRemove (index: number) {
this.dispatchEvent(new CustomEvent<PendingGrantRemovedDetail>('pending-grant-removed', {
detail: { index },
bubbles: true,
composed: true
}))
}

private getEventValue (event: Event): string {
const target = event.currentTarget as { value?: string | null } | null
return typeof target?.value === 'string' ? target.value : ''
}

protected render () {
return html`
<section class="access-grants-form" aria-label="Add access">
<p>Add person, group or software agent URL.</p>
<div class="access-grants-form-main">
<div class="access-grants-input">
<solid-ui-combobox
class="access-principal-combobox"
label="Add person, group or software agent URL."
.srOnlyLabel=${true}
.value=${this.principalInputValue}
placeholder="Paste a link or enter a name"
.asyncOptionsProvider=${this.asyncOptionsProvider}
@input=${this.onPrincipalInput}
@change=${this.onPrincipalSelect}
></solid-ui-combobox>
${this.inputError
? html`<p class="access-principal-error" role="alert">${this.inputError}</p>`
: nothing}
${this.renderPendingAccessGrants()}
</div>
<solid-ui-combobox
class="access-role-select access-role-select--top"
.value=${this.addAccessRoleValue}
@change=${this.onRoleChange}
>
${ACCESS_ROLES
.filter(role => role !== 'No Access')
.map(role => html`
<solid-ui-combobox-option value=${role}>${role}</solid-ui-combobox-option>
`)}
</solid-ui-combobox>
</div>
</section>
`
}

private renderPendingAccessGrants () {
if (!this.pendingAccessGrants.length) {
return nothing
}

return html`
<div class="access-grants-pending">
${this.pendingAccessGrants.map((grant, index) => html`
<div class="access-grants-pending-item">
<span class="access-grants-pending-item-label">${grant.label}</span>
<solid-ui-button
type="button"
variant="ghost"
class="access-grants-pending-item-remove"
@click=${() => this.onPendingGrantRemove(index)}
>
<span class="sr-only">Remove ${grant.label}</span>
<icon-lucide-circle-x slot="icon"></icon-lucide-circle-x>
</solid-ui-button>
</div>
`)}
</div>
`
}
}
48 changes: 48 additions & 0 deletions src/components/access-control-modal/AccessControlErrors.styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
:host {
display: block;
width: 100%;
contain: inline-size;

solid-ui-error-state {
width: 100%;
max-width: 100%;
contain: inline-size;
}

.access-control-save-errors {
display: grid;
gap: 8px;
width: 100%;
margin: 0;
padding-left: 0;
box-sizing: border-box;
list-style: none;

li {
display: grid;
grid-template-columns: minmax(0, 18rem) minmax(0, 1fr);
align-items: baseline;
gap: 6px;
min-width: 0;
color: var(--solid-ui-color-red-600, #be2e48);
font-size: var(--solid-ui-font-size-sm, .875rem);
line-height: 1.4;
}

.access-control-save-error-label {
font-weight: 500;
overflow-wrap: anywhere;
}

.access-control-save-error-detail {
min-width: 0;
overflow-wrap: anywhere;
}
}

@media (max-width: 600px) {
.access-control-save-errors li {
grid-template-columns: minmax(0, 40%) minmax(0, 1fr);
}
}
}
Loading
Loading