Pantrix UI

List Detail Scaffold

Bir liste ve içinde açılan öğenin ayrıntısı: yerde ikisine yer varsa yan yana, yoksa ayrıntı listenin yerinde; ayrıca ayrıntının yanında ya da üstünde, scaffold'un kendi yan sheet'inde ekstra bir panel.

import com.pantrixui.components.container.listdetail.PantrixListDetailScaffold

Örnekler

Gelen kutusu

En sık kullanılan biçim: bir ileti listesi ve açılan ileti. Liste bir iletiyi inbox.select(id) ile açar; ayrıntıya açılan iletinin anahtarı verilir.

Karttaki pencere seçicisinden "Dar"ı seç ve bir iletiye bas: ileti listenin yerini alır. Başındaki geri düğmesi, Android'in geri tuşu, masaüstünde Escape ve iOS'ta ekranın kenarından kaydırma listeye döndürür. Geri düğmesi yalnızca tek panelde durur: ayrıntı onu isSinglePane'e bakarak gösterir. listTitle ile detailTitle panelleri ekran okuyucuya tanıtır: TalkBack ileti açılınca konusunu, listeye dönülünce "Gelen kutusu"nu söyler.

"Kısa", "Geniş" ya da "Kitap"ı seç: liste ve ileti yan yana durur, açık bir ileti yokken sağda emptyDetail görünür. Bir iletiyi açıp "Dar" ile "Geniş" arasında geç: açılan ileti de listenin kaydırma yeri de kalır.

val inbox = rememberListDetailState()
PantrixListDetailScaffold(
    state = inbox,
    list = {
        MailList(opened = inbox.selectedKey, onOpen = { id -> inbox.select(id) })
    },
    detail = { id ->
        MailDetail(
            mail = MAIL.first { it.id == id },
            // Tek panelde ileti listenin yerindedir: geri düğmesi yalnızca o zaman durur.
            onBack = if (isSinglePane) ({ inbox.back() }) else null
        )
    },
    modifier = Modifier.fillMaxWidth().height(EXAMPLE_HEIGHT),
    emptyDetail = { Placeholder(text = "Okumak için bir ileti seç", icon = PantrixIcons.Inbox, tagKey = "inbox") },
    listTitle = "Gelen kutusu",
    detailTitle = { id -> MAIL.first { it.id == id }.subject },
    tagKey = "inbox"
)

@Composable
private fun MailList(
    opened: String?,
    onOpen: (String) -> Unit
) {
    PaneSurface {
        Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
            MAIL.forEachIndexed { index, mail ->
                if (index > 0) PantrixHorizontalDivider()
                ListRow(
                    title = mail.from,
                    subtitle = mail.subject,
                    selected = mail.id == opened,
                    onClick = { onOpen(mail.id) },
                    tagKey = "mail_${mail.id}"
                )
            }
        }
    }
}

@Composable
private fun MailDetail(
    mail: DemoMail,
    onBack: (() -> Unit)?
) {
    PaneSurface {
        Column(modifier = Modifier.fillMaxSize()) {
            PantrixTopBar(
                title = mail.subject,
                navigationIcon = onBack?.let { back -> { PantrixBackButton(onClick = back, tagKey = "mail_back") } },
                tagKey = "mail"
            )
            Column(
                modifier = Modifier
                    .fillMaxSize()
                    .verticalScroll(rememberScrollState())
                    .padding(PantrixTheme.spacing.lg),
                verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)
            ) {
                PantrixText(
                    text = mail.from,
                    style = PantrixTheme.typography.footnote,
                    color = PantrixTheme.colors.textSecondary
                )
                PantrixText(text = mail.body, tagKey = "mail_body")
            }
        }
    }
}

@Composable
private fun Placeholder(
    text: String,
    icon: ImageVector,
    tagKey: String
) {
    PaneSurface {
        Column(
            modifier = Modifier.align(Alignment.Center),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)
        ) {
            PantrixIcon(icon, contentDescription = null, size = 40.dp, tint = PantrixTheme.colors.textSecondary)
            PantrixText(text = text, color = PantrixTheme.colors.textSecondary, tagKey = "${tagKey}_placeholder")
        }
    }
}

private val MAIL = listOf(
    DemoMail("1", "Elif Kaya", "Yarınki toplantı", "Toplantıyı saat 10'a aldım. Sunumu bir gün önce paylaşır mısın?"),
    DemoMail("2", "Can Demir", "Sürüm notları", "1.4 sürümünün notlarını ekledim. Eksik gördüğün bir şey olursa yaz."),
    DemoMail("3", "Ayşe Yılmaz", "Tasarım incelemesi", "Yeni ayarlar ekranının taslakları hazır; perşembe bakalım mı?"),
    DemoMail("4", "Mert Şahin", "Fatura", "Ekim faturası ektedir. Ödeme ayın 30'una kadar yapılabilir."),
    DemoMail("5", "Zeynep Arslan", "Test sonuçları", "Gece çalışan testlerin hepsi geçti; tablette bir uyarı var."),
    DemoMail("6", "Deniz Aydın", "Öğle yemeği", "Cuma günü ekipçe dışarıda yiyelim mi? Yer önerisi olan yazsın."),
    DemoMail("7", "Burak Çelik", "Erişilebilirlik", "Ekran okuyucuyla bütün akışı denedim; iki düğmenin adı eksik."),
    DemoMail("8", "Selin Koç", "Yeni katılan", "Pazartesi ekibe yeni biri katılıyor; ilk haftayı planlayalım.")
)

private val EXAMPLE_HEIGHT = 460.dp

@Composable
private fun ListRow(
    title: String,
    subtitle: String?,
    selected: Boolean,
    onClick: () -> Unit,
    tagKey: String,
    initials: String? = null,
    icon: ImageVector? = null
) {
    val colors = PantrixTheme.colors
    val spacing = PantrixTheme.spacing
    val ink = if (selected) colors.primary.onContainer else Color.Unspecified
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .selectable(selected = selected, onClick = onClick)
            .background(if (selected) colors.primary.container else Color.Transparent)
            .padding(spacing.md),
        horizontalArrangement = Arrangement.spacedBy(spacing.md),
        verticalAlignment = Alignment.CenterVertically
    ) {
        if (initials != null) PantrixAvatar(contentDescription = null, initials = initials)
        if (icon != null) PantrixIcon(icon, contentDescription = null, tint = ink)
        Column(verticalArrangement = Arrangement.spacedBy(spacing.xs)) {
            PantrixText(
                text = title,
                style = PantrixTheme.typography.callout,
                color = ink,
                maxLines = 1,
                tagKey = tagKey
            )
            if (subtitle != null) {
                PantrixText(
                    text = subtitle,
                    style = PantrixTheme.typography.footnote,
                    color = if (selected) ink else colors.textSecondary,
                    maxLines = 1
                )
            }
        }
    }
}

@Composable
private fun PaneSurface(
    modifier: Modifier = Modifier,
    content: @Composable BoxScope.() -> Unit
) {
    PantrixSurface(modifier = modifier.fillMaxSize(), border = PantrixTheme.colors.border) { content() }
}

private class DemoMail(
    val id: String,
    val from: String,
    val subject: String,
    val body: String
)

Ayarlar: iki panelde ilk bölüm açık

Ayarlar ekranı: solda bölümler, sağda seçilen bölümün ayarları. İki panelde boş bir ayrıntı yerine ilk bölüm açık durur; tek panelde ise önce bölümlerin listesi görünür.

Bunun için emptyDetail bir şey çizmez, ilk bölümü açar: LaunchedEffect(Unit) { settings.select(...) }. Boş ayrıntı yalnızca iki panelde gösterildiği için bu tek panelde hiç çalışmaz.

Ayarların değerleri ayrıntıda değil, örnekte tutulur: values. Her bölüm açıldığında yeniden kurulur; değerleri ayrıntının içinde hatırlansaydı başka bir bölüme geçip dönünce kaybolurdu. "Dar"da bir anahtarı değiştir, geri dön, bölümü yeniden aç: değer yerindedir.

val settings = rememberListDetailState()
val values = remember { mutableStateMapOf<String, Boolean>() }
PantrixListDetailScaffold(
    state = settings,
    list = {
        SectionList(chosen = settings.selectedKey, onChoose = { key -> settings.select(key) })
    },
    detail = { key ->
        val section = SECTIONS.first { it.key == key }
        SettingsPage(
            section = section,
            isOn = { option -> values[option] ?: false },
            onChange = { option, on -> values[option] = on },
            onBack = if (isSinglePane) ({ settings.back() }) else null
        )
    },
    modifier = Modifier.fillMaxWidth().height(EXAMPLE_HEIGHT),
    // İki panelde boş ayrıntı yerine ilk bölüm açılır. Tek panelde boş ayrıntı gösterilmez: liste kalır.
    emptyDetail = { LaunchedEffect(Unit) { settings.select(SECTIONS.first().key) } },
    tagKey = "settings"
)

@Composable
private fun SectionList(
    chosen: String?,
    onChoose: (String) -> Unit
) {
    PaneSurface {
        Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
            SECTIONS.forEach { section ->
                ListRow(
                    title = section.title,
                    subtitle = "${section.options.size} ayar",
                    selected = section.key == chosen,
                    onClick = { onChoose(section.key) },
                    tagKey = "section_${section.key}"
                )
            }
        }
    }
}

@Composable
private fun SettingsPage(
    section: SettingsSection,
    isOn: (String) -> Boolean,
    onChange: (String, Boolean) -> Unit,
    onBack: (() -> Unit)?
) {
    PaneSurface {
        Column(modifier = Modifier.fillMaxSize()) {
            PantrixTopBar(
                title = section.title,
                navigationIcon = onBack?.let { back ->
                    { PantrixBackButton(onClick = back, tagKey = "settings_back") }
                },
                tagKey = "settings"
            )
            Column(
                modifier = Modifier
                    .fillMaxSize()
                    .verticalScroll(rememberScrollState())
                    .padding(PantrixTheme.spacing.lg),
                verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.md)
            ) {
                section.options.forEach { option ->
                    PantrixSwitchButton(
                        checked = isOn(option),
                        onCheckedChange = { onChange(option, it) },
                        modifier = Modifier.fillMaxWidth(),
                        label = option,
                        tagKey = "option_${section.key}_${section.options.indexOf(option)}"
                    )
                }
            }
        }
    }
}

private val SECTIONS = listOf(
    SettingsSection("general", "Genel", listOf("Açılışta son sayfayı göster", "Güncellemeleri kendisi kursun")),
    SettingsSection("notifications", "Bildirimler", listOf("Yeni iletiler", "Takvim hatırlatmaları", "Sesli uyarı")),
    SettingsSection("privacy", "Gizlilik", listOf("Kullanım verisi gönder", "Konumu paylaş")),
    SettingsSection("display", "Görünüm", listOf("Koyu tema", "Daha az hareket", "Büyük yazı"))
)

private val EXAMPLE_HEIGHT = 460.dp

@Composable
private fun ListRow(
    title: String,
    subtitle: String?,
    selected: Boolean,
    onClick: () -> Unit,
    tagKey: String,
    initials: String? = null,
    icon: ImageVector? = null
) {
    val colors = PantrixTheme.colors
    val spacing = PantrixTheme.spacing
    val ink = if (selected) colors.primary.onContainer else Color.Unspecified
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .selectable(selected = selected, onClick = onClick)
            .background(if (selected) colors.primary.container else Color.Transparent)
            .padding(spacing.md),
        horizontalArrangement = Arrangement.spacedBy(spacing.md),
        verticalAlignment = Alignment.CenterVertically
    ) {
        if (initials != null) PantrixAvatar(contentDescription = null, initials = initials)
        if (icon != null) PantrixIcon(icon, contentDescription = null, tint = ink)
        Column(verticalArrangement = Arrangement.spacedBy(spacing.xs)) {
            PantrixText(
                text = title,
                style = PantrixTheme.typography.callout,
                color = ink,
                maxLines = 1,
                tagKey = tagKey
            )
            if (subtitle != null) {
                PantrixText(
                    text = subtitle,
                    style = PantrixTheme.typography.footnote,
                    color = if (selected) ink else colors.textSecondary,
                    maxLines = 1
                )
            }
        }
    }
}

@Composable
private fun PaneSurface(
    modifier: Modifier = Modifier,
    content: @Composable BoxScope.() -> Unit
) {
    PantrixSurface(modifier = modifier.fillMaxSize(), border = PantrixTheme.colors.border) { content() }
}

private class SettingsSection(
    val key: String,
    val title: String,
    val options: List<String>
)

Kişiler ve yanlarında geçmişleri

Bir kişinin yanında üçüncü bir panel: geçmişi. Ayrıntıdaki "Geçmiş" düğmesi contacts.showExtra() ile onu açar.

"Geniş"i seç: üç panele yer vardır ve geçmiş ayrıntının yanında, sonda durur; başlığı ve kapatma düğmesi kendi içindedir. "Kısa"yı seç: iki panele yer vardır, geçmiş iskelenin içinde, ayrıntının üstüne sondan açılan bir sheet'te görünür. Sheet'in başlığı extraTitle'dır ve kapatma düğmesi sheet'indir; panel kendi başlığını bu yüzden isExtraInSheet'e bakarak göstermez. Scrim'e basmak, geri tuşu ve Escape sheet'i kapatır. "Dar"da da sheet'te açılır.

Sheet açıkken geçmişi kaydır, sonra "Geniş"e geç: geçmiş ayrıntının yanına geçer ve kaydırma yeri kalır. Panel iki yerde de aynı kompozisyondur.

Bir kişiden ötekine geçince geçmiş o kişinin olur; listeye dönülünce, back(), geçmiş de kapanır.

val contacts = rememberListDetailState()
PantrixListDetailScaffold(
    state = contacts,
    list = {
        ContactList(opened = contacts.selectedKey, onOpen = { id -> contacts.select(id) })
    },
    detail = { id ->
        ContactCard(
            contact = CONTACTS.first { it.id == id },
            onHistory = { contacts.showExtra() },
            onBack = if (isSinglePane) ({ contacts.back() }) else null
        )
    },
    modifier = Modifier.fillMaxWidth().height(EXAMPLE_HEIGHT),
    emptyDetail = { Placeholder(text = "Bir kişi seç", icon = PantrixIcons.Person, tagKey = "contacts") },
    extra = { id ->
        History(
            contact = CONTACTS.first { it.id == id },
            // Sheet'te başlık ve kapatma düğmesi sheet'indir.
            onClose = if (isExtraInSheet) null else ({ contacts.hideExtra() })
        )
    },
    listTitle = "Kişiler",
    detailTitle = { id -> CONTACTS.first { it.id == id }.name },
    extraTitle = "Geçmiş",
    tagKey = "contacts"
)

@Composable
private fun ContactList(
    opened: String?,
    onOpen: (String) -> Unit
) {
    PaneSurface {
        Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
            CONTACTS.forEach { contact ->
                ListRow(
                    title = contact.name,
                    subtitle = contact.role,
                    selected = contact.id == opened,
                    onClick = { onOpen(contact.id) },
                    initials = contact.initials,
                    tagKey = "contact_${contact.id}"
                )
            }
        }
    }
}

@Composable
private fun ContactCard(
    contact: DemoContact,
    onHistory: () -> Unit,
    onBack: (() -> Unit)?
) {
    val spacing = PantrixTheme.spacing
    PaneSurface {
        Column(modifier = Modifier.fillMaxSize()) {
            PantrixTopBar(
                title = contact.name,
                navigationIcon = onBack?.let { back -> { PantrixBackButton(onClick = back, tagKey = "contact_back") } },
                tagKey = "contact"
            )
            Column(
                modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(spacing.lg),
                verticalArrangement = Arrangement.spacedBy(spacing.md)
            ) {
                PantrixAvatar(contentDescription = null, initials = contact.initials, size = 64.dp)
                PantrixText(text = contact.role, color = PantrixTheme.colors.textSecondary)
                PantrixText(text = contact.email)
                PantrixButton(
                    text = "Geçmiş",
                    onClick = onHistory,
                    variant = ButtonVariant.Outline,
                    leadingIcon = { PantrixIcon(PantrixIcons.Clock, contentDescription = null) },
                    tagKey = "history"
                )
            }
        }
    }
}

@Composable
private fun History(
    contact: DemoContact,
    onClose: (() -> Unit)?
) {
    val lines = @Composable {
        Column(
            modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(PantrixTheme.spacing.lg),
            verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.md)
        ) {
            contact.history.forEach { line -> PantrixText(text = line, tagKey = "history_line") }
        }
    }
    if (onClose == null) {
        lines()
    } else {
        PaneSurface {
            Column(modifier = Modifier.fillMaxSize()) {
                PantrixTopBar(
                    title = "Geçmiş",
                    actions = {
                        PantrixIconButton(
                            imageVector = PantrixIcons.Close,
                            contentDescription = "Geçmişi kapat",
                            onClick = onClose,
                            tagKey = "history_close"
                        )
                    },
                    tagKey = "history"
                )
                lines()
            }
        }
    }
}

@Composable
private fun Placeholder(
    text: String,
    icon: ImageVector,
    tagKey: String
) {
    PaneSurface {
        Column(
            modifier = Modifier.align(Alignment.Center),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)
        ) {
            PantrixIcon(icon, contentDescription = null, size = 40.dp, tint = PantrixTheme.colors.textSecondary)
            PantrixText(text = text, color = PantrixTheme.colors.textSecondary, tagKey = "${tagKey}_placeholder")
        }
    }
}

private val CONTACTS = listOf(
    DemoContact(
        "elif",
        "Elif Kaya",
        "EK",
        "Ürün yöneticisi",
        "elif@example.com",
        listOf("Dün: yol haritası toplantısı", "Pazartesi: sürüm planı gönderildi", "Geçen hafta: kullanıcı görüşmesi")
    ),
    DemoContact(
        "can",
        "Can Demir",
        "CD",
        "Geliştirici",
        "can@example.com",
        listOf("Bugün: kod incelemesi", "Dün: hata kaydı kapatıldı")
    ),
    DemoContact(
        "ayse",
        "Ayşe Yılmaz",
        "AY",
        "Tasarımcı",
        "ayse@example.com",
        listOf("Bugün: ayarlar ekranının taslağı", "Salı: renk paleti onaylandı", "Geçen ay: tasarım sistemi")
    ),
    DemoContact(
        "mert",
        "Mert Şahin",
        "MŞ",
        "Muhasebe",
        "mert@example.com",
        listOf("Bu ay: ekim faturası", "Geçen ay: bütçe raporu")
    )
)

private val EXAMPLE_HEIGHT = 460.dp

@Composable
private fun ListRow(
    title: String,
    subtitle: String?,
    selected: Boolean,
    onClick: () -> Unit,
    tagKey: String,
    initials: String? = null,
    icon: ImageVector? = null
) {
    val colors = PantrixTheme.colors
    val spacing = PantrixTheme.spacing
    val ink = if (selected) colors.primary.onContainer else Color.Unspecified
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .selectable(selected = selected, onClick = onClick)
            .background(if (selected) colors.primary.container else Color.Transparent)
            .padding(spacing.md),
        horizontalArrangement = Arrangement.spacedBy(spacing.md),
        verticalAlignment = Alignment.CenterVertically
    ) {
        if (initials != null) PantrixAvatar(contentDescription = null, initials = initials)
        if (icon != null) PantrixIcon(icon, contentDescription = null, tint = ink)
        Column(verticalArrangement = Arrangement.spacedBy(spacing.xs)) {
            PantrixText(
                text = title,
                style = PantrixTheme.typography.callout,
                color = ink,
                maxLines = 1,
                tagKey = tagKey
            )
            if (subtitle != null) {
                PantrixText(
                    text = subtitle,
                    style = PantrixTheme.typography.footnote,
                    color = if (selected) ink else colors.textSecondary,
                    maxLines = 1
                )
            }
        }
    }
}

@Composable
private fun PaneSurface(
    modifier: Modifier = Modifier,
    content: @Composable BoxScope.() -> Unit
) {
    PantrixSurface(modifier = modifier.fillMaxSize(), border = PantrixTheme.colors.border) { content() }
}

private class DemoContact(
    val id: String,
    val name: String,
    val initials: String,
    val role: String,
    val email: String,
    val history: List<String>
)

Dosyalar: listenin genişliği sürüklenir

Listenin genişliği kullanıcının elinde: resizable = true panellerin arasına bir tutamaç koyar. Fareyle ya da parmakla sürükle; klavyede Tab ile tutamaca gel, ok tuşları 16 dp'lik adımlarla, Home ve End en dar ve en geniş hâline götürür. Ekran okuyucu tutamacı "Liste genişliği" diye okur ve değerini değiştirebilir.

Genişlik files.listWidth'te durur; ayrıntıdaki satır onu yazar, "Varsayılana dön" Dp.Unspecified ile stilin genişliğine döndürür. Stil burada liste için 280 dp ve en az 200 dp ister: ListDetailScaffoldStyle(listWidth = 280.dp, minListWidth = 200.dp). Liste, ayrıntıya en az 320 dp bırakacak kadar genişleyebilir.

Tutamaç yalnızca iki panelde durur: "Kısa" ya da "Geniş"i seç.

val files = rememberListDetailState(initialSelectedKey = FOLDERS.first().name)
PantrixListDetailScaffold(
    state = files,
    list = {
        FolderList(opened = files.selectedKey, onOpen = { name -> files.select(name) })
    },
    detail = { name ->
        FolderContents(
            folder = FOLDERS.first { it.name == name },
            listWidth = files.listWidth,
            onResetWidth = { files.listWidth = Dp.Unspecified },
            onBack = if (isSinglePane) ({ files.back() }) else null
        )
    },
    modifier = Modifier.fillMaxWidth().height(EXAMPLE_HEIGHT),
    resizable = true,
    style = ListDetailScaffoldStyle(listWidth = 280.dp, minListWidth = 200.dp),
    tagKey = "files"
)

@Composable
private fun FolderList(
    opened: String?,
    onOpen: (String) -> Unit
) {
    PaneSurface {
        Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
            FOLDERS.forEach { folder ->
                ListRow(
                    title = folder.name,
                    subtitle = "${folder.files.size} dosya",
                    selected = folder.name == opened,
                    onClick = { onOpen(folder.name) },
                    icon = PantrixIcons.Folder,
                    tagKey = "folder_${FOLDERS.indexOf(folder)}"
                )
            }
        }
    }
}

@Composable
private fun FolderContents(
    folder: DemoFolder,
    listWidth: Dp,
    onResetWidth: () -> Unit,
    onBack: (() -> Unit)?
) {
    val spacing = PantrixTheme.spacing
    PaneSurface {
        Column(modifier = Modifier.fillMaxSize()) {
            PantrixTopBar(
                title = folder.name,
                navigationIcon = onBack?.let { back -> { PantrixBackButton(onClick = back, tagKey = "folder_back") } },
                tagKey = "folder"
            )
            Column(
                modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(spacing.lg),
                verticalArrangement = Arrangement.spacedBy(spacing.sm)
            ) {
                PantrixText(
                    text = if (listWidth.isSpecified) {
                        "Listenin genişliği: ${listWidth.value.roundToInt()} dp"
                    } else {
                        "Listenin genişliği: stilinki"
                    },
                    style = PantrixTheme.typography.footnote,
                    color = PantrixTheme.colors.textSecondary,
                    tagKey = "list_width"
                )
                PantrixButton(
                    text = "Varsayılana dön",
                    onClick = onResetWidth,
                    variant = ButtonVariant.Text,
                    enabled = listWidth.isSpecified,
                    tagKey = "reset_width"
                )
                folder.files.forEach { file ->
                    Row(
                        horizontalArrangement = Arrangement.spacedBy(spacing.sm),
                        verticalAlignment = Alignment.CenterVertically
                    ) {
                        PantrixIcon(PantrixIcons.File, contentDescription = null)
                        PantrixText(text = file)
                    }
                }
            }
        }
    }
}

private val FOLDERS = listOf(
    DemoFolder("Belgeler", listOf("Sözleşme.pdf", "Toplantı notları.txt", "Bütçe 2026.xlsx")),
    DemoFolder("Fotoğraflar", listOf("Tatil 01.jpg", "Tatil 02.jpg", "Ekip fotoğrafı.png", "Logo.svg")),
    DemoFolder("Müzik", listOf("Çalma listesi.m3u", "Podcast bölümü.mp3")),
    DemoFolder("İndirilenler", listOf("Kurulum.dmg", "Rapor taslağı.docx"))
)

private val EXAMPLE_HEIGHT = 460.dp

@Composable
private fun ListRow(
    title: String,
    subtitle: String?,
    selected: Boolean,
    onClick: () -> Unit,
    tagKey: String,
    initials: String? = null,
    icon: ImageVector? = null
) {
    val colors = PantrixTheme.colors
    val spacing = PantrixTheme.spacing
    val ink = if (selected) colors.primary.onContainer else Color.Unspecified
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .selectable(selected = selected, onClick = onClick)
            .background(if (selected) colors.primary.container else Color.Transparent)
            .padding(spacing.md),
        horizontalArrangement = Arrangement.spacedBy(spacing.md),
        verticalAlignment = Alignment.CenterVertically
    ) {
        if (initials != null) PantrixAvatar(contentDescription = null, initials = initials)
        if (icon != null) PantrixIcon(icon, contentDescription = null, tint = ink)
        Column(verticalArrangement = Arrangement.spacedBy(spacing.xs)) {
            PantrixText(
                text = title,
                style = PantrixTheme.typography.callout,
                color = ink,
                maxLines = 1,
                tagKey = tagKey
            )
            if (subtitle != null) {
                PantrixText(
                    text = subtitle,
                    style = PantrixTheme.typography.footnote,
                    color = if (selected) ink else colors.textSecondary,
                    maxLines = 1
                )
            }
        }
    }
}

@Composable
private fun PaneSurface(
    modifier: Modifier = Modifier,
    content: @Composable BoxScope.() -> Unit
) {
    PantrixSurface(modifier = modifier.fillMaxSize(), border = PantrixTheme.colors.border) { content() }
}

private class DemoFolder(
    val name: String,
    val files: List<String>
)

Katlanan cihazda: kat yerinde buluşur

Katlanan bir cihazda liste ve ayrıntı kat yerinde buluşur. Her panel kendi ölçüsünü ve kendi pencere sınıfını yazar; üstündeki satır ona söyleneni gösterir. Panel kendi penceresidir: "Kitap"ta pencere Expanded'dır ama 456 dp'lik iki panelin her biri Compact okur.

"Kitap"ı seç: 951 dp'lik pencere zaten iki panellik genişliktedir, ama liste 360 dp değil, kat yerine kadardır. Aradaki boşluk kat yerinin iki yanındaki 20'şer dp'lik şerittir; hiçbir şey kat yerinin üstüne düşmez. Daha dar bir katlanabilir de kitap gibi tutulunca iki panel alır. Tutamaç yoktur: panelleri kat yeri yerleştirir.

"Masa"yı seç: kat yeri yataydır ve yan yana iki paneli ayırmaz. 669 dp'lik pencere Medium'dur: tek panel.

val notes = rememberListDetailState(initialSelectedKey = NOTES.first())
PantrixListDetailScaffold(
    state = notes,
    list = {
        Column(modifier = Modifier.fillMaxSize()) {
            Told(scope = this@PantrixListDetailScaffold, tagKey = "notes_told_list")
            MeasuredList(opened = notes.selectedKey, onOpen = { note -> notes.select(note) })
        }
    },
    detail = { note ->
        Column(modifier = Modifier.fillMaxSize()) {
            Told(scope = this@PantrixListDetailScaffold, tagKey = "notes_told_detail")
            MeasuredNote(note = note, onBack = if (isSinglePane) ({ notes.back() }) else null)
        }
    },
    modifier = Modifier.fillMaxWidth().height(EXAMPLE_HEIGHT),
    resizable = true,
    tagKey = "notes"
)

@Composable
private fun Told(
    scope: ListDetailScope,
    tagKey: String
) {
    PantrixText(
        text = "paneCount: ${scope.paneCount} · isSinglePane: ${if (scope.isSinglePane) "evet" else "hayır"}",
        modifier = Modifier.padding(PantrixTheme.spacing.md),
        style = PantrixTheme.typography.footnote,
        color = PantrixTheme.colors.textSecondary,
        tagKey = tagKey
    )
}

@Composable
private fun MeasuredList(
    opened: String?,
    onOpen: (String) -> Unit
) {
    Measured(name = "Liste", tagKey = "notes_list_size") {
        Column {
            NOTES.forEach { note ->
                ListRow(
                    title = note,
                    subtitle = null,
                    selected = note == opened,
                    onClick = { onOpen(note) },
                    tagKey = "note_${NOTES.indexOf(note)}"
                )
            }
        }
    }
}

@Composable
private fun MeasuredNote(
    note: String,
    onBack: (() -> Unit)?
) {
    PaneSurface {
        Column(modifier = Modifier.fillMaxSize()) {
            PantrixTopBar(
                title = note,
                navigationIcon = onBack?.let { back -> { PantrixBackButton(onClick = back, tagKey = "note_back") } },
                tagKey = "note"
            )
            Measured(name = "Ayrıntı", tagKey = "notes_detail_size") {}
        }
    }
}

private val NOTES = listOf("Alışveriş listesi", "Kitap önerileri", "Tatil planı", "Toplantı notları")

private val EXAMPLE_HEIGHT = 460.dp

@Composable
private fun Measured(
    name: String,
    tagKey: String,
    content: @Composable () -> Unit
) {
    var size by remember { mutableStateOf(IntSize.Zero) }
    val density = LocalDensity.current
    val words = with(density) { "${size.width.toDp().value.roundToInt()} × ${size.height.toDp().value.roundToInt()}" }
    PaneSurface(modifier = Modifier.onSizeChanged { size = it }) {
        Column(modifier = Modifier.fillMaxSize()) {
            Column(modifier = Modifier.weight(1f)) { content() }
            // Ölçüler soldan sağa yazılır: sağdan sola bir paragrafta genişlik ile yükseklik yer değiştirmiş okunurdu.
            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
                PantrixText(
                    // Panel kendi penceresidir: sınıfı panelin genişliğine göredir, pencereninkine değil.
                    text = "$name: $words dp · ${PantrixTheme.window.widthClass.name}",
                    modifier = Modifier.padding(PantrixTheme.spacing.md),
                    style = PantrixTheme.typography.footnote,
                    color = PantrixTheme.colors.textSecondary,
                    tagKey = tagKey
                )
            }
        }
    }
}

@Composable
private fun ListRow(
    title: String,
    subtitle: String?,
    selected: Boolean,
    onClick: () -> Unit,
    tagKey: String,
    initials: String? = null,
    icon: ImageVector? = null
) {
    val colors = PantrixTheme.colors
    val spacing = PantrixTheme.spacing
    val ink = if (selected) colors.primary.onContainer else Color.Unspecified
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .selectable(selected = selected, onClick = onClick)
            .background(if (selected) colors.primary.container else Color.Transparent)
            .padding(spacing.md),
        horizontalArrangement = Arrangement.spacedBy(spacing.md),
        verticalAlignment = Alignment.CenterVertically
    ) {
        if (initials != null) PantrixAvatar(contentDescription = null, initials = initials)
        if (icon != null) PantrixIcon(icon, contentDescription = null, tint = ink)
        Column(verticalArrangement = Arrangement.spacedBy(spacing.xs)) {
            PantrixText(
                text = title,
                style = PantrixTheme.typography.callout,
                color = ink,
                maxLines = 1,
                tagKey = tagKey
            )
            if (subtitle != null) {
                PantrixText(
                    text = subtitle,
                    style = PantrixTheme.typography.footnote,
                    color = if (selected) ink else colors.textSecondary,
                    maxLines = 1
                )
            }
        }
    }
}

@Composable
private fun PaneSurface(
    modifier: Modifier = Modifier,
    content: @Composable BoxScope.() -> Unit
) {
    PantrixSurface(modifier = modifier.fillMaxSize(), border = PantrixTheme.colors.border) { content() }
}

API

state

Bir liste ve içinde açılan öğenin ayrıntısı: yerde ikisine yer varsa yan yana, yoksa ayrıntı listenin yerinde; ayrıca ayrıntının yanında ya da üstünde, scaffold'un kendi yan sheet'inde ekstra bir panel.

ParametreTürVarsayılanNe olduğu
stateListDetailStateaçık olan: rememberListDetailState onu yapılandırma değişikliğinde korur.
list@Composable ListDetailScope.() -> Unitliste. Bir öğeyi ListDetailState.select ile açar.
detail@Composable ListDetailScope.(key: String) -> Unitanahtarı verilen öğenin ayrıntısı. Yeni bir anahtar onu yeniden kurar.
modifierModifierModifierpanellerin paylaştığı yer.
emptyDetail(@Composable ListDetailScope.() -> Unit)?nullaçık bir öğe yokken listenin yanındaki ayrıntı panelinde gösterilen: "Bir ileti seçin". Verilmezse orada bir şey gösterilmez.
extra(@Composable ListDetailScope.(key: String) -> Unit)?nullaçık öğe için ayrıntısının yanında üçüncü bir panel: kişinin geçmişi, bir dosyanın özellikleri. ListDetailState.showExtra çağrılınca gösterilir.
listTitleString?nulllistenin adı, görünür olurken ekran okuyucu için: "Gelen kutusu".
detailTitle((key: String) -> String)?nullanahtarı verilen öğenin ayrıntısının adı, görünür olurken ekran okuyucu için: iletinin konusu.
extraTitleString?nullekstra panelin adı: ayrıntının üstünde gösterildiği yan sheet'in başlığı, ve görünür olurken ekran okuyucu için.
resizableBooleanfalseliste ile ayrıntı arasında listeyi genişletip daraltan bir tutamaç olup olmadığı.
styleListDetailScaffoldStyle?nullscaffold'un genişlikleri, temanınkilerin üstünde.
tagKeyString?nullscaffold'un otomasyon etiketlerinin anahtarı.

ListDetailScaffoldStyle

PantrixListDetailScaffold'un ölçüleri: liste ve ekstra panel ayrıntının yanında ne kadar geniştir, tutamacı sürüklemek listeyi ve ayrıntıyı en çok ne kadar daraltabilir, ve tutamacın kendisi.

ParametreTürVarsayılanNe olduğu
listWidthDpDp.UnspecifiedTutamacı sürüklenene kadar listenin ayrıntının yanındaki genişliği: bir telefonun genişliği, 360dp.
minListWidthDpDp.UnspecifiedTutamacı sürüklemenin listeyi en çok daraltabildiği genişlik.
minDetailWidthDpDp.UnspecifiedAyrıntının listenin yanında en çok daralabildiği genişlik: tutamacın sürüklenmesi ayrıntının daha dar kalacağı yerde durur, durumda ayarlanan bir liste genişliği de.
extraWidthDpDp.UnspecifiedÜç panele yer olduğunda ekstra panelin ayrıntının yanındaki genişliği.
gapDpDp.UnspecifiedPaneller arasında. Hazır ayarlarda belirtilmez: düzenin pencereyle büyüyen sütun arası, PantrixTheme.layout.gutter. Panelleri kenar kenara buluşan, aralarında kendi çizgisi olan bir marka 0dp verir.
handleWidthDpDp.UnspecifiedGenişliği değişebilen bir listenin tutamacının boşlukta çizdiği işaret: kısa, dik bir çubuk.
handleHeightDpDp.UnspecifiedTutamacın dik çubuğunun ne kadar uzun olduğu: ne kadar kalın olduğu handleWidth'tir.
handleTargetDpDp.UnspecifiedBir parmağın ya da işaretçinin tutamacı yakaladığı şeridin genişliği; boşluğun ortasında durur, boşluk daha darsa panellerin kenarlarının üstüne taşar: parmakla her platformun en küçük dokunma hedefi, işaretçiyle dar bir şerit.